
commit
92e5418142
15 changed files with 10666 additions and 0 deletions
@ -0,0 +1,3 @@ |
|||
.idea |
|||
.vscode |
|||
node_modules |
@ -0,0 +1,21 @@ |
|||
The MIT License (MIT) |
|||
|
|||
Copyright (c) 2019-present, dexter |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
of this software and associated documentation files (the "Software"), to deal |
|||
in the Software without restriction, including without limitation the rights |
|||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
copies of the Software, and to permit persons to whom the Software is |
|||
furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in |
|||
all copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|||
THE SOFTWARE. |
@ -0,0 +1,9 @@ |
|||
module github.com/Monibuca/plugin-webrtc |
|||
|
|||
go 1.13 |
|||
|
|||
require ( |
|||
github.com/Monibuca/engine/v2 v2.0.0 |
|||
github.com/pion/rtcp v1.2.1 |
|||
github.com/pion/webrtc/v2 v2.2.14 |
|||
) |
@ -0,0 +1,105 @@ |
|||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= |
|||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= |
|||
github.com/Monibuca/engine v1.2.2 h1:hNjsrZpOmui8lYhgCJ5ltJU8g/k0Rrdysx2tHNGGnbI= |
|||
github.com/Monibuca/engine/v2 v2.0.0 h1:8FjaScrtN8QdbcxO9zZYABMC0Re3I1O1T4p94zAXYb0= |
|||
github.com/Monibuca/engine/v2 v2.0.0/go.mod h1:34EYjjV15G6myuHOKaJkO7y5tJ1Arq/NfC9Weacr2mc= |
|||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk= |
|||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= |
|||
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= |
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
|||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
|||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= |
|||
github.com/funny/slab v0.0.0-20180511031532-b1fad5e5d478 h1:Db9StoJ6RZN3YttC0Pm0I4Y5izITRYch3RMbT59BYN0= |
|||
github.com/funny/slab v0.0.0-20180511031532-b1fad5e5d478/go.mod h1:0j1+svBH8ABEIPdUP0AIg4qedsybnXGJBakCEw8cfoo= |
|||
github.com/funny/utest v0.0.0-20161029064919-43870a374500/go.mod h1:mUn39tBov9jKnTWV1RlOYoNzxdBFHiSzXWdY1FoNGGg= |
|||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= |
|||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= |
|||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= |
|||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= |
|||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= |
|||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= |
|||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= |
|||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= |
|||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs= |
|||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= |
|||
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw= |
|||
github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk= |
|||
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= |
|||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= |
|||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= |
|||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= |
|||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= |
|||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= |
|||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= |
|||
github.com/pion/datachannel v1.4.17 h1:8CChK5VrJoGrwKCysoTscoWvshCAFpUkgY11Tqgz5hE= |
|||
github.com/pion/datachannel v1.4.17/go.mod h1:+vPQfypU9vSsyPXogYj1hBThWQ6MNXEQoQAzxoPvjYM= |
|||
github.com/pion/dtls/v2 v2.0.0 h1:Fk+MBhLZ/U1bImzAhmzwbO/pP2rKhtTw8iA934H3ybE= |
|||
github.com/pion/dtls/v2 v2.0.0/go.mod h1:VkY5VL2wtsQQOG60xQ4lkV5pdn0wwBBTzCfRJqXhp3A= |
|||
github.com/pion/ice v0.7.15 h1:s1In+gnuyVq7WKWGVQL+1p+OcrMsbfL+VfSe2isH8Ag= |
|||
github.com/pion/ice v0.7.15/go.mod h1:Z6zybEQgky5mZkKcLfmvc266JukK2srz3VZBBD1iXBw= |
|||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= |
|||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= |
|||
github.com/pion/mdns v0.0.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY= |
|||
github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0= |
|||
github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k= |
|||
github.com/pion/rtcp v1.2.1 h1:S3yG4KpYAiSmBVqKAfgRa5JdwBNj4zK3RLUa8JYdhak= |
|||
github.com/pion/rtcp v1.2.1/go.mod h1:a5dj2d6BKIKHl43EnAOIrCczcjESrtPuMgfmL6/K6QM= |
|||
github.com/pion/rtp v1.5.4 h1:PuNg6xqV3brIUihatcKZj1YDUs+M45L0ZbrZWYtkDxY= |
|||
github.com/pion/rtp v1.5.4/go.mod h1:bg60AL5GotNOlYZsqycbhDtEV3TkfbpXG0KBiUq29Mg= |
|||
github.com/pion/sctp v1.7.6 h1:8qZTdJtbKfAns/Hv5L0PAj8FyXcsKhMH1pKUCGisQg4= |
|||
github.com/pion/sctp v1.7.6/go.mod h1:ichkYQ5tlgCQwEwvgfdcAolqx1nHbYCxo4D7zK/K0X8= |
|||
github.com/pion/sdp/v2 v2.3.7 h1:WUZHI3pfiYCaE8UGUYcabk863LCK+Bq3AklV5O0oInQ= |
|||
github.com/pion/sdp/v2 v2.3.7/go.mod h1:+ZZf35r1+zbaWYiZLfPutWfx58DAWcGb2QsS3D/s9M8= |
|||
github.com/pion/srtp v1.3.3 h1:8bjs9YaSNvSrbH0OfKxzPX+PTrCyAC2LoT9Qesugi+U= |
|||
github.com/pion/srtp v1.3.3/go.mod h1:jNe0jmIOqksuurR9S/7yoKDalfPeluUFrNPCBqI4FOI= |
|||
github.com/pion/stun v0.3.3 h1:brYuPl9bN9w/VM7OdNzRSLoqsnwlyNvD9MVeJrHjDQw= |
|||
github.com/pion/stun v0.3.3/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M= |
|||
github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE= |
|||
github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8= |
|||
github.com/pion/transport v0.10.0 h1:9M12BSneJm6ggGhJyWpDveFOstJsTiQjkLf4M44rm80= |
|||
github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE= |
|||
github.com/pion/turn/v2 v2.0.3 h1:SJUUIbcPoehlyZgMyIUbBBDhI03sBx32x3JuSIBKBWA= |
|||
github.com/pion/turn/v2 v2.0.3/go.mod h1:kl1hmT3NxcLynpXVnwJgObL8C9NaCyPTeqI2DcCpSZs= |
|||
github.com/pion/webrtc v1.2.0 h1:3LGGPQEMacwG2hcDfhdvwQPz315gvjZXOfY4vaF4+I4= |
|||
github.com/pion/webrtc/v2 v2.2.14 h1:bRjnXTqMDJ3VERPF45z439Sv6QfDfjdYvdQk1QcIx8M= |
|||
github.com/pion/webrtc/v2 v2.2.14/go.mod h1:G+8lShCMbHhjpMF1ZJBkyuvrxXrvW4bxs3nOt+mJ2UI= |
|||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= |
|||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= |
|||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= |
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
|||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= |
|||
github.com/shirou/gopsutil v2.20.1+incompatible h1:oIq9Cq4i84Hk8uQAUOG3eNdI/29hBawGrD5YRl6JRDY= |
|||
github.com/shirou/gopsutil v2.20.1+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= |
|||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
|||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= |
|||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= |
|||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= |
|||
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
|||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
|||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM= |
|||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= |
|||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= |
|||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
|||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= |
|||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= |
|||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= |
|||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5 h1:WQ8q63x+f/zpC8Ac1s9wLElVoHhm32p6tudrU72n1QA= |
|||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= |
|||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
|||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
|||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
|||
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
|||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= |
|||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
|||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= |
|||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
|||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
|||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
|||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= |
|||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= |
|||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
|||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
@ -0,0 +1,199 @@ |
|||
package webrtc |
|||
|
|||
import ( |
|||
"encoding/json" |
|||
"fmt" |
|||
"io/ioutil" |
|||
"net" |
|||
"net/http" |
|||
"time" |
|||
|
|||
. "github.com/Monibuca/engine/v2" |
|||
"github.com/pion/rtcp" |
|||
. "github.com/pion/webrtc/v2" |
|||
) |
|||
|
|||
var config = &struct { |
|||
ICEServers []string |
|||
}{[]string{ |
|||
"stun.l.google.com:19302", |
|||
"stun1.l.google.com:19302", |
|||
"stun2.l.google.com:19302", |
|||
"stun3.l.google.com:19302", |
|||
"stun4.l.google.com:19302", |
|||
"stun.ekiga.net", |
|||
"stun.ideasip.com", |
|||
"stun.schlund.de", |
|||
"stun.stunprotocol.org:3478", |
|||
"stun.voiparound.com", |
|||
"stun.voipbuster.com", |
|||
"stun.voipstunt.com", |
|||
"stun.voxgratia.org", |
|||
"stun.services.mozilla.com", |
|||
"stun:stun.xten.com", |
|||
"stun:stun.softjoys.com", |
|||
"stun:stunserver.org", |
|||
"stun:stun.schlund.de", |
|||
"stun:stun.rixtelecom.se", |
|||
"stun:stun.iptel.org", |
|||
"stun:stun.ideasip.com", |
|||
"stun:stun.fwdnet.net", |
|||
"stun:stun.ekiga.net", |
|||
"stun:stun01.sipphone.com", |
|||
}} |
|||
|
|||
type udpConn struct { |
|||
conn *net.UDPConn |
|||
port int |
|||
} |
|||
|
|||
func init() { |
|||
InstallPlugin(&PluginConfig{ |
|||
Config: config, |
|||
Name: "WebRTC", |
|||
Type: PLUGIN_PUBLISHER | PLUGIN_SUBSCRIBER, |
|||
Run: run, |
|||
}) |
|||
} |
|||
func run() { |
|||
m := MediaEngine{} |
|||
m.RegisterCodec(NewRTPH264Codec(DefaultPayloadTypeH264, 90000)) |
|||
api := NewAPI(WithMediaEngine(m)) |
|||
peerConnection, err := api.NewPeerConnection(Configuration{ |
|||
ICEServers: []ICEServer{ |
|||
{ |
|||
URLs: config.ICEServers, |
|||
}, |
|||
}, |
|||
}) |
|||
if err != nil { |
|||
Println(err) |
|||
return |
|||
} |
|||
// Allow us to receive 1 audio track, and 1 video track
|
|||
if _, err = peerConnection.AddTransceiverFromKind(RTPCodecTypeAudio); err != nil { |
|||
if err != nil { |
|||
Println(err) |
|||
return |
|||
} |
|||
} else if _, err = peerConnection.AddTransceiverFromKind(RTPCodecTypeVideo); err != nil { |
|||
if err != nil { |
|||
Println(err) |
|||
return |
|||
} |
|||
} |
|||
var laddr *net.UDPAddr |
|||
if laddr, err = net.ResolveUDPAddr("udp", "127.0.0.1:"); err != nil { |
|||
panic(err) |
|||
} |
|||
|
|||
// Prepare udp conns
|
|||
udpConns := map[string]*udpConn{ |
|||
"audio": {port: 4000}, |
|||
"video": {port: 4002}, |
|||
} |
|||
for _, c := range udpConns { |
|||
// Create remote addr
|
|||
var raddr *net.UDPAddr |
|||
if raddr, err = net.ResolveUDPAddr("udp", fmt.Sprintf("127.0.0.1:%d", c.port)); err != nil { |
|||
panic(err) |
|||
} |
|||
|
|||
// Dial udp
|
|||
if c.conn, err = net.DialUDP("udp", laddr, raddr); err != nil { |
|||
panic(err) |
|||
} |
|||
defer func(conn net.PacketConn) { |
|||
if closeErr := conn.Close(); closeErr != nil { |
|||
panic(closeErr) |
|||
} |
|||
}(c.conn) |
|||
} |
|||
|
|||
// Set a handler for when a new remote track starts, this handler will forward data to
|
|||
// our UDP listeners.
|
|||
// In your application this is where you would handle/process audio/video
|
|||
peerConnection.OnTrack(func(track *Track, receiver *RTPReceiver) { |
|||
// Retrieve udp connection
|
|||
c, ok := udpConns[track.Kind().String()] |
|||
if !ok { |
|||
return |
|||
} |
|||
|
|||
// Send a PLI on an interval so that the publisher is pushing a keyframe every rtcpPLIInterval
|
|||
go func() { |
|||
ticker := time.NewTicker(time.Second * 2) |
|||
for range ticker.C { |
|||
if rtcpErr := peerConnection.WriteRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: track.SSRC()}}); rtcpErr != nil { |
|||
fmt.Println(rtcpErr) |
|||
} |
|||
} |
|||
}() |
|||
|
|||
b := make([]byte, 1500) |
|||
for { |
|||
// Read
|
|||
n, readErr := track.Read(b) |
|||
if readErr != nil { |
|||
panic(readErr) |
|||
} |
|||
|
|||
// Write
|
|||
if _, err = c.conn.Write(b[:n]); err != nil { |
|||
// For this particular example, third party applications usually timeout after a short
|
|||
// amount of time during which the user doesn't have enough time to provide the answer
|
|||
// to the browser.
|
|||
// That's why, for this particular example, the user first needs to provide the answer
|
|||
// to the browser then open the third party application. Therefore we must not kill
|
|||
// the forward on "connection refused" errors
|
|||
if opError, ok := err.(*net.OpError); ok && opError.Err.Error() == "write: connection refused" { |
|||
continue |
|||
} |
|||
panic(err) |
|||
} |
|||
} |
|||
}) |
|||
|
|||
// Set the handler for ICE connection state
|
|||
// This will notify you when the peer has connected/disconnected
|
|||
peerConnection.OnICEConnectionStateChange(func(connectionState ICEConnectionState) { |
|||
fmt.Printf("Connection State has changed %s \n", connectionState.String()) |
|||
|
|||
if connectionState == ICEConnectionStateConnected { |
|||
fmt.Println("Ctrl+C the remote client to stop the demo") |
|||
} else if connectionState == ICEConnectionStateFailed || |
|||
connectionState == ICEConnectionStateDisconnected { |
|||
fmt.Println("Done forwarding") |
|||
//TODO
|
|||
} |
|||
}) |
|||
http.HandleFunc("/webrtc/answer", func(w http.ResponseWriter, r *http.Request) { |
|||
// Wait for the offer to be pasted
|
|||
offer := SessionDescription{} |
|||
bytes, err := ioutil.ReadAll(r.Body) |
|||
err = json.Unmarshal(bytes, &offer) |
|||
if err != nil { |
|||
Println(err) |
|||
} |
|||
// Set the remote SessionDescription
|
|||
if err = peerConnection.SetRemoteDescription(offer); err != nil { |
|||
panic(err) |
|||
} |
|||
|
|||
// Create answer
|
|||
answer, err := peerConnection.CreateAnswer(nil) |
|||
if err != nil { |
|||
panic(err) |
|||
} |
|||
|
|||
// Sets the LocalDescription, and starts our UDP listeners
|
|||
if err = peerConnection.SetLocalDescription(answer); err != nil { |
|||
panic(err) |
|||
} |
|||
bytes, err = json.Marshal(answer) |
|||
if err != nil { |
|||
panic(err) |
|||
} |
|||
w.Write(bytes) |
|||
}) |
|||
} |
@ -0,0 +1,19 @@ |
|||
<meta charset="utf-8"> |
|||
<title>plugin-webrtc demo</title> |
|||
<script src="https://unpkg.com/vue"></script> |
|||
<script src="./plugin-webrtc.umd.js"></script> |
|||
|
|||
<link rel="stylesheet" href="./plugin-webrtc.css"> |
|||
|
|||
|
|||
<div id="app"> |
|||
<demo></demo> |
|||
</div> |
|||
|
|||
<script> |
|||
new Vue({ |
|||
components: { |
|||
demo: plugin-webrtc |
|||
} |
|||
}).$mount('#app') |
|||
</script> |
@ -0,0 +1,408 @@ |
|||
module.exports = |
|||
/******/ (function(modules) { // webpackBootstrap
|
|||
/******/ // The module cache
|
|||
/******/ var installedModules = {}; |
|||
/******/ |
|||
/******/ // The require function
|
|||
/******/ function __webpack_require__(moduleId) { |
|||
/******/ |
|||
/******/ // Check if module is in cache
|
|||
/******/ if(installedModules[moduleId]) { |
|||
/******/ return installedModules[moduleId].exports; |
|||
/******/ } |
|||
/******/ // Create a new module (and put it into the cache)
|
|||
/******/ var module = installedModules[moduleId] = { |
|||
/******/ i: moduleId, |
|||
/******/ l: false, |
|||
/******/ exports: {} |
|||
/******/ }; |
|||
/******/ |
|||
/******/ // Execute the module function
|
|||
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); |
|||
/******/ |
|||
/******/ // Flag the module as loaded
|
|||
/******/ module.l = true; |
|||
/******/ |
|||
/******/ // Return the exports of the module
|
|||
/******/ return module.exports; |
|||
/******/ } |
|||
/******/ |
|||
/******/ |
|||
/******/ // expose the modules object (__webpack_modules__)
|
|||
/******/ __webpack_require__.m = modules; |
|||
/******/ |
|||
/******/ // expose the module cache
|
|||
/******/ __webpack_require__.c = installedModules; |
|||
/******/ |
|||
/******/ // define getter function for harmony exports
|
|||
/******/ __webpack_require__.d = function(exports, name, getter) { |
|||
/******/ if(!__webpack_require__.o(exports, name)) { |
|||
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); |
|||
/******/ } |
|||
/******/ }; |
|||
/******/ |
|||
/******/ // define __esModule on exports
|
|||
/******/ __webpack_require__.r = function(exports) { |
|||
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { |
|||
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); |
|||
/******/ } |
|||
/******/ Object.defineProperty(exports, '__esModule', { value: true }); |
|||
/******/ }; |
|||
/******/ |
|||
/******/ // create a fake namespace object
|
|||
/******/ // mode & 1: value is a module id, require it
|
|||
/******/ // mode & 2: merge all properties of value into the ns
|
|||
/******/ // mode & 4: return value when already ns object
|
|||
/******/ // mode & 8|1: behave like require
|
|||
/******/ __webpack_require__.t = function(value, mode) { |
|||
/******/ if(mode & 1) value = __webpack_require__(value); |
|||
/******/ if(mode & 8) return value; |
|||
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; |
|||
/******/ var ns = Object.create(null); |
|||
/******/ __webpack_require__.r(ns); |
|||
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); |
|||
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); |
|||
/******/ return ns; |
|||
/******/ }; |
|||
/******/ |
|||
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
|||
/******/ __webpack_require__.n = function(module) { |
|||
/******/ var getter = module && module.__esModule ? |
|||
/******/ function getDefault() { return module['default']; } : |
|||
/******/ function getModuleExports() { return module; }; |
|||
/******/ __webpack_require__.d(getter, 'a', getter); |
|||
/******/ return getter; |
|||
/******/ }; |
|||
/******/ |
|||
/******/ // Object.prototype.hasOwnProperty.call
|
|||
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; |
|||
/******/ |
|||
/******/ // __webpack_public_path__
|
|||
/******/ __webpack_require__.p = ""; |
|||
/******/ |
|||
/******/ |
|||
/******/ // Load entry module and return exports
|
|||
/******/ return __webpack_require__(__webpack_require__.s = "fb15"); |
|||
/******/ }) |
|||
/************************************************************************/ |
|||
/******/ ({ |
|||
|
|||
/***/ "8875": |
|||
/***/ (function(module, exports, __webpack_require__) { |
|||
|
|||
var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// addapted from the document.currentScript polyfill by Adam Miller
|
|||
// MIT license
|
|||
// source: https://github.com/amiller-gh/currentScript-polyfill
|
|||
|
|||
// added support for Firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1620505
|
|||
|
|||
(function (root, factory) { |
|||
if (true) { |
|||
!(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), |
|||
__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? |
|||
(__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), |
|||
__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); |
|||
} else {} |
|||
}(typeof self !== 'undefined' ? self : this, function () { |
|||
function getCurrentScript () { |
|||
if (document.currentScript) { |
|||
return document.currentScript |
|||
} |
|||
|
|||
// IE 8-10 support script readyState
|
|||
// IE 11+ & Firefox support stack trace
|
|||
try { |
|||
throw new Error(); |
|||
} |
|||
catch (err) { |
|||
// Find the second match for the "at" string to get file src url from stack.
|
|||
var ieStackRegExp = /.*at [^(]*\((.*):(.+):(.+)\)$/ig, |
|||
ffStackRegExp = /@([^@]*):(\d+):(\d+)\s*$/ig, |
|||
stackDetails = ieStackRegExp.exec(err.stack) || ffStackRegExp.exec(err.stack), |
|||
scriptLocation = (stackDetails && stackDetails[1]) || false, |
|||
line = (stackDetails && stackDetails[2]) || false, |
|||
currentLocation = document.location.href.replace(document.location.hash, ''), |
|||
pageSource, |
|||
inlineScriptSourceRegExp, |
|||
inlineScriptSource, |
|||
scripts = document.getElementsByTagName('script'); // Live NodeList collection
|
|||
|
|||
if (scriptLocation === currentLocation) { |
|||
pageSource = document.documentElement.outerHTML; |
|||
inlineScriptSourceRegExp = new RegExp('(?:[^\\n]+?\\n){0,' + (line - 2) + '}[^<]*<script>([\\d\\D]*?)<\\/script>[\\d\\D]*', 'i'); |
|||
inlineScriptSource = pageSource.replace(inlineScriptSourceRegExp, '$1').trim(); |
|||
} |
|||
|
|||
for (var i = 0; i < scripts.length; i++) { |
|||
// If ready state is interactive, return the script tag
|
|||
if (scripts[i].readyState === 'interactive') { |
|||
return scripts[i]; |
|||
} |
|||
|
|||
// If src matches, return the script tag
|
|||
if (scripts[i].src === scriptLocation) { |
|||
return scripts[i]; |
|||
} |
|||
|
|||
// If inline source matches, return the script tag
|
|||
if ( |
|||
scriptLocation === currentLocation && |
|||
scripts[i].innerHTML && |
|||
scripts[i].innerHTML.trim() === inlineScriptSource |
|||
) { |
|||
return scripts[i]; |
|||
} |
|||
} |
|||
|
|||
// If no match, return null
|
|||
return null; |
|||
} |
|||
}; |
|||
|
|||
return getCurrentScript |
|||
})); |
|||
|
|||
|
|||
/***/ }), |
|||
|
|||
/***/ "fb15": |
|||
/***/ (function(module, __webpack_exports__, __webpack_require__) { |
|||
|
|||
"use strict"; |
|||
// ESM COMPAT FLAG
|
|||
__webpack_require__.r(__webpack_exports__); |
|||
|
|||
// CONCATENATED MODULE: ./node_modules/@vue/cli-service/lib/commands/build/setPublicPath.js
|
|||
// This file is imported into lib/wc client bundles.
|
|||
|
|||
if (typeof window !== 'undefined') { |
|||
var currentScript = window.document.currentScript |
|||
if (true) { |
|||
var getCurrentScript = __webpack_require__("8875") |
|||
currentScript = getCurrentScript() |
|||
|
|||
// for backward compatibility, because previously we directly included the polyfill
|
|||
if (!('currentScript' in document)) { |
|||
Object.defineProperty(document, 'currentScript', { get: getCurrentScript }) |
|||
} |
|||
} |
|||
|
|||
var src = currentScript && currentScript.src.match(/(.+\/)[^/]+\.js(\?.*)?$/) |
|||
if (src) { |
|||
__webpack_require__.p = src[1] // eslint-disable-line
|
|||
} |
|||
} |
|||
|
|||
// Indicate to webpack that this file can be concatenated
|
|||
/* harmony default export */ var setPublicPath = (null); |
|||
|
|||
// CONCATENATED MODULE: ./node_modules/cache-loader/dist/cjs.js?{"cacheDirectory":"node_modules/.cache/vue-loader","cacheIdentifier":"46d6b4f7-vue-loader-template"}!./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/cache-loader/dist/cjs.js??ref--0-0!./node_modules/vue-loader/lib??vue-loader-options!./src/App.vue?vue&type=template&id=69b8f189&
|
|||
var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',[_vm._v(" Browser base64 Session Description "),_c('br'),_c('textarea',{attrs:{"readonly":"true"},domProps:{"value":_vm.localDescription}}),_c('br'),_vm._v("Golang base64 Session Description "),_c('br'),_c('textarea',{domProps:{"value":_vm.remoteSessionDescription}}),_c('br'),_c('button',{on:{"click":_vm.startSession}},[_vm._v("Start Session")]),_c('br'),_c('br'),_vm._v("Video "),_c('br'),_c('video',{attrs:{"id":"video1","width":"160","height":"120","autoplay":"","muted":""},domProps:{"muted":true}}),_c('br'),_vm._v("Logs "),_c('br'),_c('div',{attrs:{"id":"logs"}})])} |
|||
var staticRenderFns = [] |
|||
|
|||
|
|||
// CONCATENATED MODULE: ./src/App.vue?vue&type=template&id=69b8f189&
|
|||
|
|||
// CONCATENATED MODULE: ./node_modules/cache-loader/dist/cjs.js??ref--0-0!./node_modules/vue-loader/lib??vue-loader-options!./src/App.vue?vue&type=script&lang=js&
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
|
|||
let pc = new RTCPeerConnection({ |
|||
iceServers: [ |
|||
{ |
|||
urls: "stun:stun.l.google.com:19302" |
|||
} |
|||
] |
|||
}); |
|||
/* harmony default export */ var Appvue_type_script_lang_js_ = ({ |
|||
data() { |
|||
return { |
|||
localDescription: "", |
|||
remoteSessionDescription: "" |
|||
}; |
|||
}, |
|||
methods: { |
|||
startSession() { |
|||
this.ajax.post("/webrtc/answer", this.localDescription).then(result => { |
|||
this.remoteSessionDescription = result; |
|||
pc.setRemoteDescription(new RTCSessionDescription(result)); |
|||
}); |
|||
} |
|||
}, |
|||
mounted() { |
|||
/* eslint-env browser */ |
|||
var log = msg => { |
|||
document.getElementById("logs").innerHTML += msg + "<br>"; |
|||
}; |
|||
|
|||
navigator.mediaDevices |
|||
.getUserMedia({ video: true, audio: true }) |
|||
.then(stream => { |
|||
pc.addStream((document.getElementById("video1").srcObject = stream)); |
|||
pc.createOffer() |
|||
.then(d => pc.setLocalDescription(d)) |
|||
.catch(log); |
|||
}) |
|||
.catch(log); |
|||
|
|||
pc.oniceconnectionstatechange = e => log(pc.iceConnectionState); |
|||
pc.onicecandidate = event => { |
|||
if (event.candidate === null) { |
|||
this.localDescription = JSON.stringify(pc.localDescription); |
|||
} |
|||
}; |
|||
} |
|||
}); |
|||
|
|||
// CONCATENATED MODULE: ./src/App.vue?vue&type=script&lang=js&
|
|||
/* harmony default export */ var src_Appvue_type_script_lang_js_ = (Appvue_type_script_lang_js_); |
|||
// CONCATENATED MODULE: ./node_modules/vue-loader/lib/runtime/componentNormalizer.js
|
|||
/* globals __VUE_SSR_CONTEXT__ */ |
|||
|
|||
// IMPORTANT: Do NOT use ES2015 features in this file (except for modules).
|
|||
// This module is a runtime utility for cleaner component module output and will
|
|||
// be included in the final webpack user bundle.
|
|||
|
|||
function normalizeComponent ( |
|||
scriptExports, |
|||
render, |
|||
staticRenderFns, |
|||
functionalTemplate, |
|||
injectStyles, |
|||
scopeId, |
|||
moduleIdentifier, /* server only */ |
|||
shadowMode /* vue-cli only */ |
|||
) { |
|||
// Vue.extend constructor export interop
|
|||
var options = typeof scriptExports === 'function' |
|||
? scriptExports.options |
|||
: scriptExports |
|||
|
|||
// render functions
|
|||
if (render) { |
|||
options.render = render |
|||
options.staticRenderFns = staticRenderFns |
|||
options._compiled = true |
|||
} |
|||
|
|||
// functional template
|
|||
if (functionalTemplate) { |
|||
options.functional = true |
|||
} |
|||
|
|||
// scopedId
|
|||
if (scopeId) { |
|||
options._scopeId = 'data-v-' + scopeId |
|||
} |
|||
|
|||
var hook |
|||
if (moduleIdentifier) { // server build
|
|||
hook = function (context) { |
|||
// 2.3 injection
|
|||
context = |
|||
context || // cached call
|
|||
(this.$vnode && this.$vnode.ssrContext) || // stateful
|
|||
(this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext) // functional
|
|||
// 2.2 with runInNewContext: true
|
|||
if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') { |
|||
context = __VUE_SSR_CONTEXT__ |
|||
} |
|||
// inject component styles
|
|||
if (injectStyles) { |
|||
injectStyles.call(this, context) |
|||
} |
|||
// register component module identifier for async chunk inferrence
|
|||
if (context && context._registeredComponents) { |
|||
context._registeredComponents.add(moduleIdentifier) |
|||
} |
|||
} |
|||
// used by ssr in case component is cached and beforeCreate
|
|||
// never gets called
|
|||
options._ssrRegister = hook |
|||
} else if (injectStyles) { |
|||
hook = shadowMode |
|||
? function () { |
|||
injectStyles.call( |
|||
this, |
|||
(options.functional ? this.parent : this).$root.$options.shadowRoot |
|||
) |
|||
} |
|||
: injectStyles |
|||
} |
|||
|
|||
if (hook) { |
|||
if (options.functional) { |
|||
// for template-only hot-reload because in that case the render fn doesn't
|
|||
// go through the normalizer
|
|||
options._injectStyles = hook |
|||
// register for functional component in vue file
|
|||
var originalRender = options.render |
|||
options.render = function renderWithStyleInjection (h, context) { |
|||
hook.call(context) |
|||
return originalRender(h, context) |
|||
} |
|||
} else { |
|||
// inject component registration as beforeCreate hook
|
|||
var existing = options.beforeCreate |
|||
options.beforeCreate = existing |
|||
? [].concat(existing, hook) |
|||
: [hook] |
|||
} |
|||
} |
|||
|
|||
return { |
|||
exports: scriptExports, |
|||
options: options |
|||
} |
|||
} |
|||
|
|||
// CONCATENATED MODULE: ./src/App.vue
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
/* normalize component */ |
|||
|
|||
var component = normalizeComponent( |
|||
src_Appvue_type_script_lang_js_, |
|||
render, |
|||
staticRenderFns, |
|||
false, |
|||
null, |
|||
null, |
|||
null |
|||
|
|||
) |
|||
|
|||
/* harmony default export */ var App = (component.exports); |
|||
// CONCATENATED MODULE: ./node_modules/@vue/cli-service/lib/commands/build/entry-lib.js
|
|||
|
|||
|
|||
/* harmony default export */ var entry_lib = __webpack_exports__["default"] = (App); |
|||
|
|||
|
|||
|
|||
/***/ }) |
|||
|
|||
/******/ })["default"]; |
|||
//# sourceMappingURL=plugin-webrtc.common.js.map
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,418 @@ |
|||
(function webpackUniversalModuleDefinition(root, factory) { |
|||
if(typeof exports === 'object' && typeof module === 'object') |
|||
module.exports = factory(); |
|||
else if(typeof define === 'function' && define.amd) |
|||
define([], factory); |
|||
else if(typeof exports === 'object') |
|||
exports["plugin-webrtc"] = factory(); |
|||
else |
|||
root["plugin-webrtc"] = factory(); |
|||
})((typeof self !== 'undefined' ? self : this), function() { |
|||
return /******/ (function(modules) { // webpackBootstrap
|
|||
/******/ // The module cache
|
|||
/******/ var installedModules = {}; |
|||
/******/ |
|||
/******/ // The require function
|
|||
/******/ function __webpack_require__(moduleId) { |
|||
/******/ |
|||
/******/ // Check if module is in cache
|
|||
/******/ if(installedModules[moduleId]) { |
|||
/******/ return installedModules[moduleId].exports; |
|||
/******/ } |
|||
/******/ // Create a new module (and put it into the cache)
|
|||
/******/ var module = installedModules[moduleId] = { |
|||
/******/ i: moduleId, |
|||
/******/ l: false, |
|||
/******/ exports: {} |
|||
/******/ }; |
|||
/******/ |
|||
/******/ // Execute the module function
|
|||
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); |
|||
/******/ |
|||
/******/ // Flag the module as loaded
|
|||
/******/ module.l = true; |
|||
/******/ |
|||
/******/ // Return the exports of the module
|
|||
/******/ return module.exports; |
|||
/******/ } |
|||
/******/ |
|||
/******/ |
|||
/******/ // expose the modules object (__webpack_modules__)
|
|||
/******/ __webpack_require__.m = modules; |
|||
/******/ |
|||
/******/ // expose the module cache
|
|||
/******/ __webpack_require__.c = installedModules; |
|||
/******/ |
|||
/******/ // define getter function for harmony exports
|
|||
/******/ __webpack_require__.d = function(exports, name, getter) { |
|||
/******/ if(!__webpack_require__.o(exports, name)) { |
|||
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); |
|||
/******/ } |
|||
/******/ }; |
|||
/******/ |
|||
/******/ // define __esModule on exports
|
|||
/******/ __webpack_require__.r = function(exports) { |
|||
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { |
|||
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); |
|||
/******/ } |
|||
/******/ Object.defineProperty(exports, '__esModule', { value: true }); |
|||
/******/ }; |
|||
/******/ |
|||
/******/ // create a fake namespace object
|
|||
/******/ // mode & 1: value is a module id, require it
|
|||
/******/ // mode & 2: merge all properties of value into the ns
|
|||
/******/ // mode & 4: return value when already ns object
|
|||
/******/ // mode & 8|1: behave like require
|
|||
/******/ __webpack_require__.t = function(value, mode) { |
|||
/******/ if(mode & 1) value = __webpack_require__(value); |
|||
/******/ if(mode & 8) return value; |
|||
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; |
|||
/******/ var ns = Object.create(null); |
|||
/******/ __webpack_require__.r(ns); |
|||
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); |
|||
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); |
|||
/******/ return ns; |
|||
/******/ }; |
|||
/******/ |
|||
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
|||
/******/ __webpack_require__.n = function(module) { |
|||
/******/ var getter = module && module.__esModule ? |
|||
/******/ function getDefault() { return module['default']; } : |
|||
/******/ function getModuleExports() { return module; }; |
|||
/******/ __webpack_require__.d(getter, 'a', getter); |
|||
/******/ return getter; |
|||
/******/ }; |
|||
/******/ |
|||
/******/ // Object.prototype.hasOwnProperty.call
|
|||
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; |
|||
/******/ |
|||
/******/ // __webpack_public_path__
|
|||
/******/ __webpack_require__.p = ""; |
|||
/******/ |
|||
/******/ |
|||
/******/ // Load entry module and return exports
|
|||
/******/ return __webpack_require__(__webpack_require__.s = "fb15"); |
|||
/******/ }) |
|||
/************************************************************************/ |
|||
/******/ ({ |
|||
|
|||
/***/ "8875": |
|||
/***/ (function(module, exports, __webpack_require__) { |
|||
|
|||
var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// addapted from the document.currentScript polyfill by Adam Miller
|
|||
// MIT license
|
|||
// source: https://github.com/amiller-gh/currentScript-polyfill
|
|||
|
|||
// added support for Firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1620505
|
|||
|
|||
(function (root, factory) { |
|||
if (true) { |
|||
!(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), |
|||
__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? |
|||
(__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), |
|||
__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); |
|||
} else {} |
|||
}(typeof self !== 'undefined' ? self : this, function () { |
|||
function getCurrentScript () { |
|||
if (document.currentScript) { |
|||
return document.currentScript |
|||
} |
|||
|
|||
// IE 8-10 support script readyState
|
|||
// IE 11+ & Firefox support stack trace
|
|||
try { |
|||
throw new Error(); |
|||
} |
|||
catch (err) { |
|||
// Find the second match for the "at" string to get file src url from stack.
|
|||
var ieStackRegExp = /.*at [^(]*\((.*):(.+):(.+)\)$/ig, |
|||
ffStackRegExp = /@([^@]*):(\d+):(\d+)\s*$/ig, |
|||
stackDetails = ieStackRegExp.exec(err.stack) || ffStackRegExp.exec(err.stack), |
|||
scriptLocation = (stackDetails && stackDetails[1]) || false, |
|||
line = (stackDetails && stackDetails[2]) || false, |
|||
currentLocation = document.location.href.replace(document.location.hash, ''), |
|||
pageSource, |
|||
inlineScriptSourceRegExp, |
|||
inlineScriptSource, |
|||
scripts = document.getElementsByTagName('script'); // Live NodeList collection
|
|||
|
|||
if (scriptLocation === currentLocation) { |
|||
pageSource = document.documentElement.outerHTML; |
|||
inlineScriptSourceRegExp = new RegExp('(?:[^\\n]+?\\n){0,' + (line - 2) + '}[^<]*<script>([\\d\\D]*?)<\\/script>[\\d\\D]*', 'i'); |
|||
inlineScriptSource = pageSource.replace(inlineScriptSourceRegExp, '$1').trim(); |
|||
} |
|||
|
|||
for (var i = 0; i < scripts.length; i++) { |
|||
// If ready state is interactive, return the script tag
|
|||
if (scripts[i].readyState === 'interactive') { |
|||
return scripts[i]; |
|||
} |
|||
|
|||
// If src matches, return the script tag
|
|||
if (scripts[i].src === scriptLocation) { |
|||
return scripts[i]; |
|||
} |
|||
|
|||
// If inline source matches, return the script tag
|
|||
if ( |
|||
scriptLocation === currentLocation && |
|||
scripts[i].innerHTML && |
|||
scripts[i].innerHTML.trim() === inlineScriptSource |
|||
) { |
|||
return scripts[i]; |
|||
} |
|||
} |
|||
|
|||
// If no match, return null
|
|||
return null; |
|||
} |
|||
}; |
|||
|
|||
return getCurrentScript |
|||
})); |
|||
|
|||
|
|||
/***/ }), |
|||
|
|||
/***/ "fb15": |
|||
/***/ (function(module, __webpack_exports__, __webpack_require__) { |
|||
|
|||
"use strict"; |
|||
// ESM COMPAT FLAG
|
|||
__webpack_require__.r(__webpack_exports__); |
|||
|
|||
// CONCATENATED MODULE: ./node_modules/@vue/cli-service/lib/commands/build/setPublicPath.js
|
|||
// This file is imported into lib/wc client bundles.
|
|||
|
|||
if (typeof window !== 'undefined') { |
|||
var currentScript = window.document.currentScript |
|||
if (true) { |
|||
var getCurrentScript = __webpack_require__("8875") |
|||
currentScript = getCurrentScript() |
|||
|
|||
// for backward compatibility, because previously we directly included the polyfill
|
|||
if (!('currentScript' in document)) { |
|||
Object.defineProperty(document, 'currentScript', { get: getCurrentScript }) |
|||
} |
|||
} |
|||
|
|||
var src = currentScript && currentScript.src.match(/(.+\/)[^/]+\.js(\?.*)?$/) |
|||
if (src) { |
|||
__webpack_require__.p = src[1] // eslint-disable-line
|
|||
} |
|||
} |
|||
|
|||
// Indicate to webpack that this file can be concatenated
|
|||
/* harmony default export */ var setPublicPath = (null); |
|||
|
|||
// CONCATENATED MODULE: ./node_modules/cache-loader/dist/cjs.js?{"cacheDirectory":"node_modules/.cache/vue-loader","cacheIdentifier":"46d6b4f7-vue-loader-template"}!./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/cache-loader/dist/cjs.js??ref--0-0!./node_modules/vue-loader/lib??vue-loader-options!./src/App.vue?vue&type=template&id=69b8f189&
|
|||
var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',[_vm._v(" Browser base64 Session Description "),_c('br'),_c('textarea',{attrs:{"readonly":"true"},domProps:{"value":_vm.localDescription}}),_c('br'),_vm._v("Golang base64 Session Description "),_c('br'),_c('textarea',{domProps:{"value":_vm.remoteSessionDescription}}),_c('br'),_c('button',{on:{"click":_vm.startSession}},[_vm._v("Start Session")]),_c('br'),_c('br'),_vm._v("Video "),_c('br'),_c('video',{attrs:{"id":"video1","width":"160","height":"120","autoplay":"","muted":""},domProps:{"muted":true}}),_c('br'),_vm._v("Logs "),_c('br'),_c('div',{attrs:{"id":"logs"}})])} |
|||
var staticRenderFns = [] |
|||
|
|||
|
|||
// CONCATENATED MODULE: ./src/App.vue?vue&type=template&id=69b8f189&
|
|||
|
|||
// CONCATENATED MODULE: ./node_modules/cache-loader/dist/cjs.js??ref--0-0!./node_modules/vue-loader/lib??vue-loader-options!./src/App.vue?vue&type=script&lang=js&
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
//
|
|||
|
|||
let pc = new RTCPeerConnection({ |
|||
iceServers: [ |
|||
{ |
|||
urls: "stun:stun.l.google.com:19302" |
|||
} |
|||
] |
|||
}); |
|||
/* harmony default export */ var Appvue_type_script_lang_js_ = ({ |
|||
data() { |
|||
return { |
|||
localDescription: "", |
|||
remoteSessionDescription: "" |
|||
}; |
|||
}, |
|||
methods: { |
|||
startSession() { |
|||
this.ajax.post("/webrtc/answer", this.localDescription).then(result => { |
|||
this.remoteSessionDescription = result; |
|||
pc.setRemoteDescription(new RTCSessionDescription(result)); |
|||
}); |
|||
} |
|||
}, |
|||
mounted() { |
|||
/* eslint-env browser */ |
|||
var log = msg => { |
|||
document.getElementById("logs").innerHTML += msg + "<br>"; |
|||
}; |
|||
|
|||
navigator.mediaDevices |
|||
.getUserMedia({ video: true, audio: true }) |
|||
.then(stream => { |
|||
pc.addStream((document.getElementById("video1").srcObject = stream)); |
|||
pc.createOffer() |
|||
.then(d => pc.setLocalDescription(d)) |
|||
.catch(log); |
|||
}) |
|||
.catch(log); |
|||
|
|||
pc.oniceconnectionstatechange = e => log(pc.iceConnectionState); |
|||
pc.onicecandidate = event => { |
|||
if (event.candidate === null) { |
|||
this.localDescription = JSON.stringify(pc.localDescription); |
|||
} |
|||
}; |
|||
} |
|||
}); |
|||
|
|||
// CONCATENATED MODULE: ./src/App.vue?vue&type=script&lang=js&
|
|||
/* harmony default export */ var src_Appvue_type_script_lang_js_ = (Appvue_type_script_lang_js_); |
|||
// CONCATENATED MODULE: ./node_modules/vue-loader/lib/runtime/componentNormalizer.js
|
|||
/* globals __VUE_SSR_CONTEXT__ */ |
|||
|
|||
// IMPORTANT: Do NOT use ES2015 features in this file (except for modules).
|
|||
// This module is a runtime utility for cleaner component module output and will
|
|||
// be included in the final webpack user bundle.
|
|||
|
|||
function normalizeComponent ( |
|||
scriptExports, |
|||
render, |
|||
staticRenderFns, |
|||
functionalTemplate, |
|||
injectStyles, |
|||
scopeId, |
|||
moduleIdentifier, /* server only */ |
|||
shadowMode /* vue-cli only */ |
|||
) { |
|||
// Vue.extend constructor export interop
|
|||
var options = typeof scriptExports === 'function' |
|||
? scriptExports.options |
|||
: scriptExports |
|||
|
|||
// render functions
|
|||
if (render) { |
|||
options.render = render |
|||
options.staticRenderFns = staticRenderFns |
|||
options._compiled = true |
|||
} |
|||
|
|||
// functional template
|
|||
if (functionalTemplate) { |
|||
options.functional = true |
|||
} |
|||
|
|||
// scopedId
|
|||
if (scopeId) { |
|||
options._scopeId = 'data-v-' + scopeId |
|||
} |
|||
|
|||
var hook |
|||
if (moduleIdentifier) { // server build
|
|||
hook = function (context) { |
|||
// 2.3 injection
|
|||
context = |
|||
context || // cached call
|
|||
(this.$vnode && this.$vnode.ssrContext) || // stateful
|
|||
(this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext) // functional
|
|||
// 2.2 with runInNewContext: true
|
|||
if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') { |
|||
context = __VUE_SSR_CONTEXT__ |
|||
} |
|||
// inject component styles
|
|||
if (injectStyles) { |
|||
injectStyles.call(this, context) |
|||
} |
|||
// register component module identifier for async chunk inferrence
|
|||
if (context && context._registeredComponents) { |
|||
context._registeredComponents.add(moduleIdentifier) |
|||
} |
|||
} |
|||
// used by ssr in case component is cached and beforeCreate
|
|||
// never gets called
|
|||
options._ssrRegister = hook |
|||
} else if (injectStyles) { |
|||
hook = shadowMode |
|||
? function () { |
|||
injectStyles.call( |
|||
this, |
|||
(options.functional ? this.parent : this).$root.$options.shadowRoot |
|||
) |
|||
} |
|||
: injectStyles |
|||
} |
|||
|
|||
if (hook) { |
|||
if (options.functional) { |
|||
// for template-only hot-reload because in that case the render fn doesn't
|
|||
// go through the normalizer
|
|||
options._injectStyles = hook |
|||
// register for functional component in vue file
|
|||
var originalRender = options.render |
|||
options.render = function renderWithStyleInjection (h, context) { |
|||
hook.call(context) |
|||
return originalRender(h, context) |
|||
} |
|||
} else { |
|||
// inject component registration as beforeCreate hook
|
|||
var existing = options.beforeCreate |
|||
options.beforeCreate = existing |
|||
? [].concat(existing, hook) |
|||
: [hook] |
|||
} |
|||
} |
|||
|
|||
return { |
|||
exports: scriptExports, |
|||
options: options |
|||
} |
|||
} |
|||
|
|||
// CONCATENATED MODULE: ./src/App.vue
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
/* normalize component */ |
|||
|
|||
var component = normalizeComponent( |
|||
src_Appvue_type_script_lang_js_, |
|||
render, |
|||
staticRenderFns, |
|||
false, |
|||
null, |
|||
null, |
|||
null |
|||
|
|||
) |
|||
|
|||
/* harmony default export */ var App = (component.exports); |
|||
// CONCATENATED MODULE: ./node_modules/@vue/cli-service/lib/commands/build/entry-lib.js
|
|||
|
|||
|
|||
/* harmony default export */ var entry_lib = __webpack_exports__["default"] = (App); |
|||
|
|||
|
|||
|
|||
/***/ }) |
|||
|
|||
/******/ })["default"]; |
|||
}); |
|||
//# sourceMappingURL=plugin-webrtc.umd.js.map
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,2 @@ |
|||
(function(e,t){"object"===typeof exports&&"object"===typeof module?module.exports=t():"function"===typeof define&&define.amd?define([],t):"object"===typeof exports?exports["plugin-webrtc"]=t():e["plugin-webrtc"]=t()})("undefined"!==typeof self?self:this,(function(){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s="fb15")}({8875:function(e,t,n){var r,o,i;(function(n,c){o=[],r=c,i="function"===typeof r?r.apply(t,o):r,void 0===i||(e.exports=i)})("undefined"!==typeof self&&self,(function(){function e(){if(document.currentScript)return document.currentScript;try{throw new Error}catch(l){var e,t,n,r=/.*at [^(]*\((.*):(.+):(.+)\)$/gi,o=/@([^@]*):(\d+):(\d+)\s*$/gi,i=r.exec(l.stack)||o.exec(l.stack),c=i&&i[1]||!1,s=i&&i[2]||!1,a=document.location.href.replace(document.location.hash,""),u=document.getElementsByTagName("script");c===a&&(e=document.documentElement.outerHTML,t=new RegExp("(?:[^\\n]+?\\n){0,"+(s-2)+"}[^<]*<script>([\\d\\D]*?)<\\/script>[\\d\\D]*","i"),n=e.replace(t,"$1").trim());for(var d=0;d<u.length;d++){if("interactive"===u[d].readyState)return u[d];if(u[d].src===c)return u[d];if(c===a&&u[d].innerHTML&&u[d].innerHTML.trim()===n)return u[d]}return null}}return e}))},fb15:function(e,t,n){"use strict";if(n.r(t),"undefined"!==typeof window){var r=window.document.currentScript,o=n("8875");r=o(),"currentScript"in document||Object.defineProperty(document,"currentScript",{get:o});var i=r&&r.src.match(/(.+\/)[^/]+\.js(\?.*)?$/);i&&(n.p=i[1])}var c=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",[e._v(" Browser base64 Session Description "),n("br"),n("textarea",{attrs:{readonly:"true"},domProps:{value:e.localDescription}}),n("br"),e._v("Golang base64 Session Description "),n("br"),n("textarea",{domProps:{value:e.remoteSessionDescription}}),n("br"),n("button",{on:{click:e.startSession}},[e._v("Start Session")]),n("br"),n("br"),e._v("Video "),n("br"),n("video",{attrs:{id:"video1",width:"160",height:"120",autoplay:"",muted:""},domProps:{muted:!0}}),n("br"),e._v("Logs "),n("br"),n("div",{attrs:{id:"logs"}})])},s=[];let a=new RTCPeerConnection({iceServers:[{urls:"stun:stun.l.google.com:19302"}]});var u={data(){return{localDescription:"",remoteSessionDescription:""}},methods:{startSession(){this.ajax.post("/webrtc/answer",this.localDescription).then(e=>{this.remoteSessionDescription=e,a.setRemoteDescription(new RTCSessionDescription(e))})}},mounted(){var e=e=>{document.getElementById("logs").innerHTML+=e+"<br>"};navigator.mediaDevices.getUserMedia({video:!0,audio:!0}).then(t=>{a.addStream(document.getElementById("video1").srcObject=t),a.createOffer().then(e=>a.setLocalDescription(e)).catch(e)}).catch(e),a.oniceconnectionstatechange=t=>e(a.iceConnectionState),a.onicecandidate=e=>{null===e.candidate&&(this.localDescription=JSON.stringify(a.localDescription))}}},d=u;function l(e,t,n,r,o,i,c,s){var a,u="function"===typeof e?e.options:e;if(t&&(u.render=t,u.staticRenderFns=n,u._compiled=!0),r&&(u.functional=!0),i&&(u._scopeId="data-v-"+i),c?(a=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"===typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),o&&o.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(c)},u._ssrRegister=a):o&&(a=s?function(){o.call(this,(u.functional?this.parent:this).$root.$options.shadowRoot)}:o),a)if(u.functional){u._injectStyles=a;var d=u.render;u.render=function(e,t){return a.call(t),d(e,t)}}else{var l=u.beforeCreate;u.beforeCreate=l?[].concat(l,a):[a]}return{exports:e,options:u}}var f=l(d,c,s,!1,null,null,null),p=f.exports;t["default"]=p}})["default"]})); |
|||
//# sourceMappingURL=plugin-webrtc.umd.min.js.map
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
@ -0,0 +1,15 @@ |
|||
{ |
|||
"name": "dashboard", |
|||
"version": "1.0.0", |
|||
"description": "dashboard of webrtc plugin for monibuca", |
|||
"main": "index.js", |
|||
"scripts": { |
|||
"build": "vue-cli-service build --target lib --name plugin-webrtc" |
|||
}, |
|||
"author": "dexter", |
|||
"license": "ISC", |
|||
"devDependencies": { |
|||
"@vue/cli-service": "^4.2.3", |
|||
"vue-template-compiler": "^2.6.11" |
|||
} |
|||
} |
@ -0,0 +1,72 @@ |
|||
<template> |
|||
<div> |
|||
Browser base64 Session Description |
|||
<br /> |
|||
<textarea readonly="true" :value="localDescription"></textarea> |
|||
<br />Golang base64 Session Description |
|||
<br /> |
|||
<textarea :value="remoteSessionDescription"></textarea> |
|||
<br /> |
|||
<button @click="startSession">Start Session</button> |
|||
<br /> |
|||
|
|||
<br />Video |
|||
<br /> |
|||
<video id="video1" width="160" height="120" autoplay muted></video> |
|||
<br />Logs |
|||
<br /> |
|||
<div id="logs"></div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
let pc = new RTCPeerConnection({ |
|||
iceServers: [ |
|||
{ |
|||
urls: "stun:stun.l.google.com:19302" |
|||
} |
|||
] |
|||
}); |
|||
export default { |
|||
data() { |
|||
return { |
|||
localDescription: "", |
|||
remoteSessionDescription: "" |
|||
}; |
|||
}, |
|||
methods: { |
|||
startSession() { |
|||
this.ajax.post("/webrtc/answer", this.localDescription).then(result => { |
|||
this.remoteSessionDescription = result; |
|||
pc.setRemoteDescription(new RTCSessionDescription(result)); |
|||
}); |
|||
} |
|||
}, |
|||
mounted() { |
|||
/* eslint-env browser */ |
|||
var log = msg => { |
|||
document.getElementById("logs").innerHTML += msg + "<br>"; |
|||
}; |
|||
|
|||
navigator.mediaDevices |
|||
.getUserMedia({ video: true, audio: true }) |
|||
.then(stream => { |
|||
pc.addStream((document.getElementById("video1").srcObject = stream)); |
|||
pc.createOffer() |
|||
.then(d => pc.setLocalDescription(d)) |
|||
.catch(log); |
|||
}) |
|||
.catch(log); |
|||
|
|||
pc.oniceconnectionstatechange = e => log(pc.iceConnectionState); |
|||
pc.onicecandidate = event => { |
|||
if (event.candidate === null) { |
|||
this.localDescription = JSON.stringify(pc.localDescription); |
|||
} |
|||
}; |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style> |
|||
</style> |
Loading…
Reference in new issue