diff --git a/node/Makefile b/node/Makefile index ff93a88c7415e7622b95416808ed60548ddbfe6b..3985b0a4d815e598a3c31fadd2bbad60b879dd56 100644 --- a/node/Makefile +++ b/node/Makefile @@ -1,10 +1,13 @@ SOURCES = \ static/css/style.css \ + static/css/player.css \ static/css/bootstrap.min.css \ static/js/bootstrap.bundle.min.js \ static/js/jquery-3.5.1.slim.min.js \ static/js/autoradio.js \ - static/autoradio.svg + static/js/player.js \ + static/autoradio.svg \ + static/speaker.svg COMPRESSED = \ $(SOURCES:%=%.br) \ diff --git a/node/bindata.go b/node/bindata.go index ca69fac222ed3eb7ce33be963d185710b7afdf3b..5050cd98889e50eb093aaf5ef58821f7abcafb0a 100644 --- a/node/bindata.go +++ b/node/bindata.go @@ -6,6 +6,9 @@ // static/css/bootstrap.min.css // static/css/bootstrap.min.css.br // static/css/bootstrap.min.css.gz +// static/css/player.css +// static/css/player.css.br +// static/css/player.css.gz // static/css/style.css // static/css/style.css.br // static/css/style.css.gz @@ -19,8 +22,15 @@ // static/js/jquery-3.5.1.slim.min.js // static/js/jquery-3.5.1.slim.min.js.br // static/js/jquery-3.5.1.slim.min.js.gz +// static/js/player.js +// static/js/player.js.br +// static/js/player.js.gz // static/radio52.png +// static/speaker.svg +// static/speaker.svg.br +// static/speaker.svg.gz // templates/index.html +// templates/player.html // DO NOT EDIT! package node @@ -192,6 +202,223 @@ func staticCssBootstrapMinCssGz() (*asset, error) { return a, nil } +var _staticCssPlayerCss = []byte(`#player { + display: block; + position: absolute; + width: 375px; +} + +.button { + display: block; + width: 0; + height: 0; + border-top: 35px solid transparent; + border-bottom: 35px solid transparent; + border-left: 60px solid orangered; + margin: 70px auto; + position: relative; + z-index: 1; + transition: all .4; + -webkit-transition: all .4; + -moz-transition: all .4; + left: 10px; +} + +.button:before { + content: ''; + position: absolute; + top: -75px; + left: -115px; + bottom: -75px; + right: -35px; + border-radius: 50%; + border: 15px solid orangered; + z-index: 2; + transition: all .4s; + -webkit-transition: all .4; + -moz-transition: all .4; + transition: transform .3s; +} + +.loading:before { + animation: pulse .5s ease-in infinite; +} + +@keyframes pulse { + 0% { box-shadow: 0px 0px 0px red; } + 50% { box-shadow: 0px 0px 55px red; } +} + +.button:after { + content:''; + opacity:0; + transition: opacity .4s; +} + +.button:hover:before, +.button.play:before { + transform: scale(1.2); + -webkit-transform: scale(1.2); + -moz-transform: scale(1.2); +} + +.button.pause:after { + content: ''; + opacity: 1; + width: 50px; + height: 70px; + background: orangered; + position:absolute; + right: 1px; + top: -35px; + border-left: 25px solid orangered; + box-shadow: inset 25px 0 0 0 #fff; +} + + +/** VOLUME SLIDER **/ + +input[type=range] { + height: 58px; + -webkit-appearance: none; + margin: 5px; + width: 80%; + max-width: 260px; + background-color: transparent; +} +input[type=range]:focus { + outline: none; +} +input[type=range]::-webkit-slider-runnable-track { + width: 100%; + height: 6px; + cursor: pointer; + animate: 0.2s; + background: #FF0000; + border-radius: 3px; + border: 0px solid #F27B7F; +} +input[type=range]::-webkit-slider-thumb { + box-shadow: 0px 0px 0px #A6A6A6; + border: 2px solid #968994; + height: 50px; + width: 17px; + border-radius: 2px; + background: black; + cursor: pointer; + -webkit-appearance: none; + margin-top: -23px; +} +input[type=range]:focus::-webkit-slider-runnable-track { + background: #FF0000; +} +input[type=range]::-moz-range-track { + width: 100%; + height: 6px; + cursor: pointer; + animate: 0.2s; + box-shadow: 0px 0px 0px #A6A6A6; + background: #FF0000; + border-radius: 3px; + border: 0px solid #F27B7F; +} +input[type=range]::-moz-range-thumb { + box-shadow: 0px 0px 0px #A6A6A6; + border: 2px solid #968994; + height: 50px; + width: 17px; + border-radius: 2px; + background: black; + cursor: pointer; +} +input[type=range]::-ms-track { + width: 100%; + height: 6px; + cursor: pointer; + animate: 0.2s; + background: transparent; + border-color: transparent; + color: transparent; +} +input[type=range]::-ms-fill-lower { + background: #FF0000; + border: 0px solid #F27B7F; + border-radius: 6px; + box-shadow: 0px 0px 0px #A6A6A6; +} +input[type=range]::-ms-fill-upper { + background: #FF0000; + border: 0px solid #F27B7F; + border-radius: 6px; + box-shadow: 0px 0px 0px #A6A6A6; +} +input[type=range]::-ms-thumb { + margin-top: 1px; + box-shadow: 0px 0px 0px #A6A6A6; + border: 2px solid #968994; + height: 50px; + width: 17px; + border-radius: 2px; + background: black; + cursor: pointer; +} +input[type=range]:focus::-ms-fill-lower { + background: #FF0000; +} +input[type=range]:focus::-ms-fill-upper { + background: #FF0000; +} +`) + +func staticCssPlayerCssBytes() ([]byte, error) { + return _staticCssPlayerCss, nil +} + +func staticCssPlayerCss() (*asset, error) { + bytes, err := staticCssPlayerCssBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "static/css/player.css", size: 3355, mode: os.FileMode(420), modTime: time.Unix(1612343832, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _staticCssPlayerCssBr = []byte("\xc1\xd0h\x00b1`\xb7)7\xc2\u2b6e\x90\xa6\x9cm\u0154\xfb\u0600tc\xd8W\xaa\xcd\xe0\x84\vK6\x88\x02L\u07e5\xb5\xa5\x8a\xd9\xce\xdf\xd6\xd0RH\xaf:DA7\u05a2\u0655`\xea&\xe100\xdd\xd8\xe0.\u05d2\xd9\xf8\x8e\xefx\x94\xb5U\xbe\x93\xcd\xfe\xce\u0726\x00\xa0\x00%g\x93K\x99\xdd\tW\xf2\xbe\xda\xfd\xbc\xaa\xd0]\x86\u04f9\x06\xca\"6\xd1\xdfXg\x1f\x1b}3\x1a\x8d\x1a\x01\xa8\xafK(A\xc7\x1ei%Pz>X\xa0\xecoLvd\xc4y-,\x1a\xdb\x14<\xa3\xad\xe7U\xd8x\u029aZl\xe1s\xf0\xee#\xb6\x98h\xb1O\x0eq4\xd1r\x8d0\xcbW\xc2t\x92N\x82\xcdM\xb9R#\x97\x16s\x1b!\xbfj\xc4\x1a\xf5\xff|P\xe3}u\xfb\x99i\xac\x8b\xa4\xe4\xaeA\x8dB&\xa3\f\xb3\x1e\xb0,a\x01W\x19Vv\xf33 %\f\xf9)\xaaa\xceQ\x0f\x9a\x96\xbf\x89\x1f\xe5wB\x96\x98\x97$#i\xf1X0\xadM\xea\xaai,\x86\x14\x17'Y\x11\x13\xd1p\x1cc|\x03\x10\x18n\xef\xac(\x1e\xb1\xa1{.\xb7\xc2\x1a\xe5\x12\xe7\xc3q\v\v_\xc6o\xfb7T\x17\x97{\xc4\xdcWp\xafX\xb6\x8eD\xf7\x87\x8a\x05\x03\xfe\xd3\xf3YUG,k,\x84\r6\xee\x1a\xa5F\xa6u\xbbw\xe0H\xc7B`\xaah\x10|\x1dR\u07fd\x8333R\xc9\x12b\x1d\xe3O\x1e\x9d\u040c\xb5x|\x95h\xeb\xdf\x00:t\xc1\xc9\xfe\x9db\x1aO5\xbeIz\x0f\x88Cr\xde\xe3\xc0D\xb0p\xa9\xa0\xc7\xd3\t-g9!S\xa9\xd4\u0485\xb4GYR\xae+9\x8ao\xb5Y\xec!)P\xe4/\x82\x1b\xff'p\xd5\xff\x16\x9f\x9d\xa0\t\"?\xcd\u05c4\x03\xde\xecE1\x85\b\xac\xb5\\b\xbf\xe5\xcd\x19\xef\x9c?\x96\v\x03\u0309Y\x93\xae\x98\x17\x0f\xbd\x006\xa4ln\xf9\xae\x01H\xefy\u01b1b\x85\x16\xc0\rC!\"\xe0\xaf\xde-|u\xfc_\x82\u071b&21\xe4\xc4\u01fehB\xa2b\x13l\xec\x91\xeb\x95\x19\xbae%\xdd\xd9\xea\xd6\x18\t\x06\x81dp\xddr\x99\x14B\x14\xb8\xfc\x14\xeb&i\xd5#F?az\xc19\u0705\x04\xc8\x1f\x00\xf3X\xc6>>\xdd\xef^\\Z\x13\xd5\xc0\xb3]\xcf3\xb6dK\x05q\x82\x1d\x80\x13\xffR%\x89\x85\xd8\xed\xe5R\xb8\x9a\x18P\xf2*\xa5\xe3J:X\x84~\x01\xf8\xb9\tn\x01\u03a6\x89\x06\x04\xca\"\u0355\x84\b\xcbx\n\x12\x8f\x8c?If\xe9\xca\xc8p\xe2w9w\x18\xc1\u38ac\xae\x0f0\xe4\u0471t1@\xa2\x1eV\x96\xdaA\xf2\vB\xea\xbf\xf0\xaeGj)\x8d?\x92\x93\u032a\xa9\xe0\x97\x0eG\xfc\xe1\x8dt\xc0;") + +func staticCssPlayerCssBrBytes() ([]byte, error) { + return _staticCssPlayerCssBr, nil +} + +func staticCssPlayerCssBr() (*asset, error) { + bytes, err := staticCssPlayerCssBrBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "static/css/player.css.br", size: 695, mode: os.FileMode(420), modTime: time.Unix(1612345238, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _staticCssPlayerCssGz = []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x02\x03\xd5T\xf7\x96\xb3*\x10\xff?O\xc1\xed\xd5|\xea~i\xee\xed\xbd\xf7\xdeQ\u01c4\x13\x02\x1e\xc0\xadg\xdf\xfd\"\xb3\xb8\x92\r9\xb7\x97\xada\x1cg~e\x98\xc7ZN/A\x91\xeb\t\xb1_5\xd3\xfd\xb9 %\x97\xd5\xf6\xd4\xc5Z\xa9\x99aR\x14\x84\x96Z\xf2\xce\x00\xc6\xcfYm6\x059Y\xcc\u068b\xd3\xc9\xcdd2-;c\xa4\x88\x97\xf2\xaf\xa4x\xda\x00[o\xccp,\xa5\xaaA%F\xb6\xb6\xa8\xadIl3V\x13\xa3\xa8\xd0-U L\x90WJ\xdbl\xf7\x9bR94\xb6\xcd<\x1d\x12\xa5M\\\x83\x82\x1a\xd3vT\xad\x99\xe5\xb7\xe83hg\xe4>q\x05\x9c\x1av\x06\x18\xbfJ\x98\xa8\xe1\xa2 \x19\x9e][/\x11\xe7d\xfa\x10\xe3\xc99\x94[f\x92\xe8\U000ddf0a>D\xccY\x1aj[\x94\xd0H\x05\xb7\x12WR\x18\x106\xed\xa9\xa7\x8e[\xe54M\u0428\xbb\xe2I\x96\r\x11T3HR\xe8N\xd2\v\x1c\u0229h\xcd:]\x90Y\xfa\xc48n\xd1\xceb\n\x0f\x92\xe51\xc9\xf4_\xa0\xd9(\x8e\x9f\xadV;2=\xd1(!\x97\x16\xb8X\x87\x1aR\xc1v\x14_i;\xae\x81Lg\x9a\x00\xd5`\x11\x13&\x1a&\x98\x01\xf7\xfe+[\xb8l\x14\u0741v\x99\xbe@\xfa\x04\xb9\xb6\n\\$zCky^\x10k\xd9\xf0\xdb\v@n\\\xde,\x9a8\x9b\r\x99c\xabic@\xed9\ud356-\xad\x98\xb9,\xd2{\xc4\xfd#\x94tTn#\xcf@\xddr\u007f\xdeG\xa7\xee\x8a\x06\x82\f\xc2\x15DW\x94\xc3\xd3\xd94\u007f&4'\x9a\x82\xfe\u011e#\x18lK;\r\a\x19\x92}\x8a$\vv\xc7\xcc*\x16\xae\x8f\xc5\x10)i\xb5]+\u0649\xba\x18\r`x3\x82\x8b\xe1g<\xf3\x15\x8cl\x83\x89\x0f\x16H\x1e\x1b\xef\xc0U&4\x18\xccM\xdd\xf7cM\xd38\xf2\x93\a\xcf>K\xbe\xfa\xf8\x83/?|\x93|\xfe\xc1\xbbo\xbc\xf9\x19y\xf6\xd9\a\x93\t\x13mg\xbe7\x97-\xbc\xe8\xca\xfe\xe8$\xf1\xf4fK\x04\xe3\u0167m\v\xd4\xe6UP\x10!\x85#\xe2\x17\x18\xe2\xf6R-\xd3'\xf0\xe1Er\x1b\xc9\xe7\xa8\xd5H\xa9\xa4\x92\\\xaa\"\u071d7\xf71\x15\x8d\xac:\xed\x90\xc9\xcep&|\xfb\x83\u0245G\xab9s+\xa3\x13\x82\x96\x1c\x12\xa3l\u7f8a\a\x99\xa5\x88\xd2\u04dd#\xc0\xaaS\xba\x87\xd5J&\f\xa8>\x847\u0576M\xa7\xb9\x0eI\x14\u4c77\xdeJ\xed\x97\v\x87[\xea\x04\v\xfa\x1d\x95z\x0f\xed+\xf9\xe2\xb5\xc5[\xbf\x8d\x80\xd9t\xbb\xd2\xe1\x8e]\xf5\xc7^\x9d\xf7\xdf\xe3^\xf9]\xaf\xd5|\xb9Z=\x1c\x13\xf5\x83\xecuX\xe01@\xefJ\xecS-\xb9=\x1c\xd0\xe8\xb7\rI\x82#\x9e;Y\xa2>\xff\x06\x03\x0f\xc9\x1f\xd1\xd2-\x05w\xf8k\xfd?\xee\xc4\xdf<\"!\xad\xff\xfexDI\xe8\xbf\xedR\x06;\xc5c?\xb8p\b9\x10\x8d\x03n\x18\xe7\t\x97\u7822\x93x\xdc\xcf}!\x91\xe1op\xef8\xa4\xaem\xffC\x90\xfcP\x86W?\u00f2\xff\xb39\u016d\xf4\x9b\u034f\x97\xf8Mfa\x89_\x01\xe01$V\x1b\r\x00\x00") + +func staticCssPlayerCssGzBytes() ([]byte, error) { + return _staticCssPlayerCssGz, nil +} + +func staticCssPlayerCssGz() (*asset, error) { + bytes, err := staticCssPlayerCssGzBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "static/css/player.css.gz", size: 815, mode: os.FileMode(420), modTime: time.Unix(1612344943, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + var _staticCssStyleCss = []byte(`/* Space out content a bit */ body { padding-top: 80px; @@ -442,7 +669,7 @@ func staticJsJquery351SlimMinJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "static/js/jquery-3.5.1.slim.min.js", size: 72380, mode: os.FileMode(420), modTime: time.Unix(1588633359, 0)} + info := bindataFileInfo{name: "static/js/jquery-3.5.1.slim.min.js", size: 72380, mode: os.FileMode(420), modTime: time.Unix(1612307980, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -459,7 +686,7 @@ func staticJsJquery351SlimMinJsBr() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "static/js/jquery-3.5.1.slim.min.js.br", size: 22370, mode: os.FileMode(420), modTime: time.Unix(1588633359, 0)} + info := bindataFileInfo{name: "static/js/jquery-3.5.1.slim.min.js.br", size: 22370, mode: os.FileMode(420), modTime: time.Unix(1612307980, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -476,7 +703,97 @@ func staticJsJquery351SlimMinJsGz() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "static/js/jquery-3.5.1.slim.min.js.gz", size: 24600, mode: os.FileMode(420), modTime: time.Unix(1588633359, 0)} + info := bindataFileInfo{name: "static/js/jquery-3.5.1.slim.min.js.gz", size: 24600, mode: os.FileMode(420), modTime: time.Unix(1612307980, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _staticJsPlayerJs = []byte(`var spplayer = {}; + +spplayer.init = function(p) { + let status = "pause"; + const player = document.createElement("audio"); + const el_button = p.getElementsByClassName("button")[0]; + const el_volume = p.getElementsByClassName("volume")[0]; + + // Create a source with parameters extracted from the target + // element's attributes. + let source = document.createElement("source"); + source.type = p.getAttribute('stream-type'); + source.src = p.getAttribute('stream-src'); + player.appendChild(source); + + // Enough of the audio has loaded to allow playback to begin. + player.addEventListener("canplaythrough", function () { + el_button.classList.remove("loading"); + }); + + el_button.addEventListener("click", function () { + if (status === "play") { + player.load(); + } else { + player.play(); + } + status = status === "play" ? "pause" : "play"; + el_button.classList.toggle("pause"); + }); + + changeVolume = function (v) { + player.volume = el_volume.value/100; + }; + + el_volume.addEventListener("mousemove", changeVolume); + el_volume.addEventListener("change", changeVolume); +}; + +spplayer.init(document.getElementById("player")); +`) + +func staticJsPlayerJsBytes() ([]byte, error) { + return _staticJsPlayerJs, nil +} + +func staticJsPlayerJs() (*asset, error) { + bytes, err := staticJsPlayerJsBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "static/js/player.js", size: 1240, mode: os.FileMode(420), modTime: time.Unix(1612344201, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _staticJsPlayerJsBr = []byte("\xb1\xb8&\x00a\x1c\xa6\x1b{\xb9\xc4]\xd4fx\u0272\xb9\x94\x9dL\x89-\x1bW\x8f\xbcW\xf5\xf9\x06Q\xe8\xb1{\xb4\xcd^\xc8\xe9\xe4\x0f\x98\xa8\x96\x80j\x9c\xb4\xb8P\xb7\x80\xb7#zp\x12h;\x94\x1b\xb9\x81\x00\x1aWo\x86\xa6\xb7\xdfM\xf0\xfbv\xfc\x02`\x1f\x14\b\xfc\x89\x02\xef\u64c1Y\x00t\xd7)\n$\xbdp](\x9b\xf6\xaa\xd1A\x1b\r\xfb\xe5\xc7/\n\xf7\x95k\xf9\xed\u3684\u008ct\xc2'\x03k\xa7\x96[\x1c\x0e\xa3\xe8-\x93\x87\r~&\x19K\x91\xe7\xbf\xdcJ\x9cO\x16USvI\u0152\x1f\x9bG,E\x84\xbdZ\x83`x\xce\x18\xad\xb0\x00\xc6\xee\xdfx\xa3i\x85\x8a\x19\x00$X8_\xca\xdb\r\xd6\xf2v=9\xc7\x1e\x04D\xa4\xe3\x18\x85\xde\u015a\u03ed\xd0[O\u030d\xe0\x13\xb4`B!9\x95X\x18\x91\xb1\x90\x8b\xe3\xfd\xde{v\xcdy2\a\xf5+}\xe97\xcaFb\xa1\x11f\v\x90\xe0?1\xb4\xc5\xe8W\x06k\xff\x05\x1747\u0398\xb0\xde\x18B!Q\u047c\xdfP\xc8\x18a\xacZ\x81\x00-{\x02sI77p\xd8goe-\xb9\xe3\xccy\x8c*\x9fFH\x8f\u035eY\x86\xaaF\xcb\xc6s\\P\xbe\xb7\xea\u0530\x9a\xa0\xf5\xa1Md\xc9 \xc9&\xfc\xb8\x10\xa7(\x89c\xd2<\x10\x187s\xe3Y\x05x\xabN_\x02\x92\xce;6a\xb8\xac\x9c\x8bR&\xfb\x9c\x91\xe0\xeet\x9c,") + +func staticJsPlayerJsBrBytes() ([]byte, error) { + return _staticJsPlayerJsBr, nil +} + +func staticJsPlayerJsBr() (*asset, error) { + bytes, err := staticJsPlayerJsBrBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "static/js/player.js.br", size: 370, mode: os.FileMode(420), modTime: time.Unix(1612345238, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _staticJsPlayerJsGz = []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x02\x03\x85T\u0152\xe3<\x10\xbe\xe7)\xba\x82\xf2\xff\xef\x06\x8e\xcb\xccp[FE\xee\u062a\x91%\x97\xd4\x1e\x9ew_\x93\x14\x06\xb3\xfd\x81\x9ar\xca-\xb8<W\xfc\x02-<\x80\xab\x9b{\x9d\x8e\u007f\x1eK-\xa9|\xb9(\xb4 i4\xcb#\xb8\xea@\xf9SH\xe0\x88S\xe1\xca\xcf\u075c\x17\x0e\xbb\xf7\xea/\xc2hG\x10\xf4b#\x8a\f5\x8d\x85EN\xf8Ba\xf5\u013a\xbc\x88\xa5\xe9F\xab\x1cT\u007f\xe6\x05\x91\xd1%-\x1f'H-\xd8=\xbdx\xa6\xb8s\x1fy\x86\xac\xdb@\xba\u044f\xe9\xaf\r\xf2\xa9Q\xa5\xd5Ar\x03i\xc95{2\x81g\xf5\u0280\x833\x85\x15\bg\x92R\u0239-\x19\x84\xd6\x01\x9e\x93\xe5\x820\x86\x855\x19P\x8a@\u0716\x16^\x00\x1b\xab\x91\x03Nde\xb9Btc\x9f&\xaf\xba?\x15\r\xc0\xe7\xa2y\x1a\xd3E\x1eBy\xe2U\xd9\xc8Q\xc9\xcdnW_G\xeb\x04g\xc5~|\xf9\xd1\xc3\xdb\xca\xf2<G\x1d?K\xa5\x8aY\xa3\x10-3\xf2B\x9b\"I\xc1,\xea`\xebRA\xca\x1d(\xc3c\x8c\x81\fp\xa5\xccY\xad5\xe7\xe2\xa4z3\xc7D\xea\xf1\x9aE\x1c\xbf8EM\xef\xa5#\xd4hYWp]}\xa3\xd4V\xfa\xdd[\xa1\xb1\x80\xf9\u01aa~\xa1\x11\u01a2*]\xc5\x1f[\xcc\xcc)\xb2n\xb5\x04\xa9\x13\x9f\xad\x1b\xbf\xea\xc0\xd9e\xab\xa48\xd9\xeb&\x17\xc0|+?(\x9b\xb9Zaw\x15\xb0\x12R\xe5\xceZ\xeb\u06be\xf4u\xb8\x1b[]\u05b0\xe1.\f\u0396-<\xf2\xc3\x04w\xdbW\xf7\x0e\xa6\x85L\x92(d\ri+)\"\xe5:\xc1\xcf~.\x96\xf1\x9f\x86\xf8\x96\xeb\r\xe3\x13Fi|\xcaU\x81\x93\xd9t\xda\uadb2K\xc0v\xae3S\xb8\xbaTe\xbeW\xed\xa3{G\xa9\r|\x9b\xb7\xf5\x8f\xc4\xc2$-\xe7\xfc\xe9\u015b\x98u\x1bP7*Y\xff\x00\x92\xc7V\xf8\xd8\x04\x00\x00") + +func staticJsPlayerJsGzBytes() ([]byte, error) { + return _staticJsPlayerJsGz, nil +} + +func staticJsPlayerJsGz() (*asset, error) { + bytes, err := staticJsPlayerJsGzBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "static/js/player.js.gz", size: 502, mode: os.FileMode(420), modTime: time.Unix(1612344943, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -498,6 +815,64 @@ func staticRadio52Png() (*asset, error) { return a, nil } +var _staticSpeakerSvg = []byte(`<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="500" height="500" viewBox="0 0 75 75"> +<path d="M39.389,13.769 L22.235,28.606 L6,28.606 L6,47.699 L21.989,47.699 L39.389,62.75 L39.389,13.769z" +style="stroke:#FF4500;stroke-width:5;stroke-linejoin:round;fill:#FF4500;" +/> +<path d="M48,27.6a19.5,19.5 0 0 1 0,21.4M55.1,20.5a30,30 0 0 1 0,35.6M61.6,14a38.8,38.8 0 0 1 0,48.6" style="fill:none;stroke:#FF4500;stroke-width:5;stroke-linecap:round"/> +</svg> +`) + +func staticSpeakerSvgBytes() ([]byte, error) { + return _staticSpeakerSvg, nil +} + +func staticSpeakerSvg() (*asset, error) { + bytes, err := staticSpeakerSvgBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "static/speaker.svg", size: 524, mode: os.FileMode(420), modTime: time.Unix(1612341006, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _staticSpeakerSvgBr = []byte("\xa1X\x10\x00\xe08\xc8\u0351\xd7\x14#\xe1\xf0\x16\xf4\xe7~\v\x89\xd0w\xa3\xbe\xd39f\x89_\x02\"\x1a\n!\xf2\u007f7\xc1\xee}\xe3s\\\xa39,\x8a\x86\xc8\x04:-\xc28\xcf5\u04f9\x12\x9e\x85\x84\x01%\xb5^\xe9\x13\a\x1d$h\x191\xf7T\"\xab\xd8\xfdE\xf6\x04G\x97\x9bn\x9c\xccp\xbe\xd8\xee72\xfe\xe3\xbd\xf9\xbb\xfc\xa2\x1d\u0449\x02\xb3\x8fS\u02e8\xccLTg\x96;\xf7+?0(:A\xd0\u0583\u02f7.\u0630Ia0\xf6N$\xec\xfdq\u066e\\A\x97\v\x88\xc3\x14\x8b\x99\xaa\xa2\xba\x00\x9a1r4\xd3x\x06>\x99B6\t\x96\\\xa68\x13\x88\x1a\x85\xc6X\xde\xfek\xafR\xbc=\x9e\xf7\xcba\u0568O&>073wa\x14\u020a\x12\xad\u0606\xa9\x99\xa6}\u02ab\xae\xb6F\xe0\x9f}\x06M\x18gR0\xc0\x811R~\x16\u00e0\x82\xbe\n\x01\x05\x941\xcc\x1c\x83c\x82\xb9\v\x18\xab(\t\a\xf13\x971\x83\xec\xe0\xb3\f[\x14\x04\xf5\xd5uD\xe8\x17\xb3k\xb1\x161\xd1W\xda\x01") + +func staticSpeakerSvgBrBytes() ([]byte, error) { + return _staticSpeakerSvgBr, nil +} + +func staticSpeakerSvgBr() (*asset, error) { + bytes, err := staticSpeakerSvgBrBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "static/speaker.svg.br", size: 278, mode: os.FileMode(420), modTime: time.Unix(1612345238, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _staticSpeakerSvgGz = []byte("\x1f\x8b\b\x00\x00\x00\x00\x00\x02\x03\x8c\x91E\x96\xe30\x10@\xd7Cw\u042b\xd9*e\x81%\xf3@\xd3*\xd9u\x1f\xc0a\x94b\b\x9e\xbe\xe5p\xb2j[T\xfc\xebU\xfaw\xbb\x98\x93\xf5\xa0\xac&\xd6d\xc0\x91\x01\x19\x98\x9e\xedO\xcc(\x83\x8f\xf7\xb7V\b\xa4\xaas\xd3\xcf\xe7\xd6\f20\x16\xfe\xfe\xf9\xf5=\xad\xd6#\xe2BM\x95\xc1\xb8\xae\x97\xb1\xe7m6\x1b\xdcH\xb4\xe5\xc8\x13\x8c1\xcfy\xc0Cf\xb2\x99\xf4\xebq\x06\x8a9a<\x98\x8c\xc6\xf5IXO\x06\x9b'\xbb\u0340\x11F\x02\xe5\x164U\x96y=&\xfd\f:2B\x19F\x94K\ftD\xdaB\xa0\x90\x8a\x8a\x105\u04e4\xado^~\x80:j\\8Fat\x11O\t\xb4@\x97\xbd}\x97n\x0f\xbf\xbeW\xf5n\ueeab\xea\xd2\xce\x06\xf1\xef\xb77\xdfa%G\xb1u\xa0\x8e\xd5Y\x9cO\xcc`j'&.\xed\xca\xf4\x93\xe1d>\xbfD\xb8T\xde\x1d\xb8\x1fR\xe1\x10r\x1e\xa1\xa2\xcdA\x98\xfb9a\xd4\x01\xfa\x1d\xa5\x90S\xc1P\xe5\x92Q\xc9.F\xa9Pw4GM\xb9\x9f\xcb\x10C\xda\x1c\x17\xb3\xef\xda\x05r\x82>\x00\x187\x9d\xe4\xcb\xf8\xbd|y\xa4\x87\x03m3\xab\xcf\xd9q\x01\x00id\x0f\x85\f\x02\x00\x00") + +func staticSpeakerSvgGzBytes() ([]byte, error) { + return _staticSpeakerSvgGz, nil +} + +func staticSpeakerSvgGz() (*asset, error) { + bytes, err := staticSpeakerSvgGzBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "static/speaker.svg.gz", size: 329, mode: os.FileMode(420), modTime: time.Unix(1612344943, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + var _templatesIndexHtml = []byte(`<!DOCTYPE html> <html lang="en"> <head> @@ -525,26 +900,25 @@ var _templatesIndexHtml = []byte(`<!DOCTYPE html> <div class="col-lg-6"> <h4>Streams</h4> <ul> - {{$domain := .Domain}} {{range $m := .Mounts}} <li> - <a href="http://{{$domain}}{{$m.Mount.Path}}" + <a href="/player/{{$m.Mount.Path}}" {{if $m.Mount.RelayUrl}} data-toggle="tooltip" data-delay="300" title="relay of {{$m.Mount.RelayUrl}}" {{else if $m.IcecastMount.GetDescription}} data-toggle="tooltip" data-delay="300" title="{{$m.IcecastMount.GetDescription}}" {{end}} >{{$m.Mount.Path}}</a> - <a href="http://{{$domain}}{{$m.Mount.Path}}.m3u">(m3u)</a> + <a href="/{{$m.Mount.Path}}.m3u">(m3u)</a> <span class="badge badge-secondary">{{$m.Listeners}}</span> {{if $m.TransMounts}} <ul> {{range $tm := $m.TransMounts}} <li> - <a href="http://{{$domain}}{{$tm.Mount.Path}}" + <a href="/player/{{$tm.Mount.Path}}" data-toggle="tooltip" data-delay="300" title="{{$tm.Mount.TranscodeParams.String}}" >{{$tm.Mount.Path}}</a> - <a href="http://{{$domain}}{{$tm.Mount.Path}}.m3u">(m3u)</a> + <a href="/{{$tm.Mount.Path}}.m3u">(m3u)</a> <span class="badge badge-secondary">{{$tm.Listeners}}</span> </li> {{end}} @@ -602,7 +976,53 @@ func templatesIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "templates/index.html", size: 3037, mode: os.FileMode(420), modTime: time.Unix(1612307759, 0)} + info := bindataFileInfo{name: "templates/index.html", size: 2948, mode: os.FileMode(420), modTime: time.Unix(1612345109, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _templatesPlayerHtml = []byte(`<!DOCTYPE html> +<html lang="en"> + <head> + <title>{{.Domain}}</title> + <link rel="stylesheet" href="/static/css/bootstrap.min.css"> + <link rel="stylesheet" href="/static/css/style.css"> + <link rel="stylesheet" href="/static/css/player.css"> + <link rel="shortcut icon" href="/static/radio52.png"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + </head> + <body> + + <div class="container"> + <div class="page-header"> + <h1>{{.Name}}</h1> + </div> + + <div id="player" stream-type="{{.Type}}" stream-src="{{.URL}}"> + <a href="#" title="Listen" class="button loading"></a> + <div class="volumebar"> + <img src="/static/speaker.svg" width="60" height="60" alt="volume"> + <input class="volume" type="range" min="0" max="100" value="100" > + </div> + </div> + </div> + + <script type="text/javascript" src="/static/js/player.js"></script> + </body> +</html> +`) + +func templatesPlayerHtmlBytes() ([]byte, error) { + return _templatesPlayerHtml, nil +} + +func templatesPlayerHtml() (*asset, error) { + bytes, err := templatesPlayerHtmlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "templates/player.html", size: 955, mode: os.FileMode(420), modTime: time.Unix(1612344550, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -665,6 +1085,9 @@ var _bindata = map[string]func() (*asset, error){ "static/css/bootstrap.min.css": staticCssBootstrapMinCss, "static/css/bootstrap.min.css.br": staticCssBootstrapMinCssBr, "static/css/bootstrap.min.css.gz": staticCssBootstrapMinCssGz, + "static/css/player.css": staticCssPlayerCss, + "static/css/player.css.br": staticCssPlayerCssBr, + "static/css/player.css.gz": staticCssPlayerCssGz, "static/css/style.css": staticCssStyleCss, "static/css/style.css.br": staticCssStyleCssBr, "static/css/style.css.gz": staticCssStyleCssGz, @@ -678,8 +1101,15 @@ var _bindata = map[string]func() (*asset, error){ "static/js/jquery-3.5.1.slim.min.js": staticJsJquery351SlimMinJs, "static/js/jquery-3.5.1.slim.min.js.br": staticJsJquery351SlimMinJsBr, "static/js/jquery-3.5.1.slim.min.js.gz": staticJsJquery351SlimMinJsGz, + "static/js/player.js": staticJsPlayerJs, + "static/js/player.js.br": staticJsPlayerJsBr, + "static/js/player.js.gz": staticJsPlayerJsGz, "static/radio52.png": staticRadio52Png, + "static/speaker.svg": staticSpeakerSvg, + "static/speaker.svg.br": staticSpeakerSvgBr, + "static/speaker.svg.gz": staticSpeakerSvgGz, "templates/index.html": templatesIndexHtml, + "templates/player.html": templatesPlayerHtml, } // AssetDir returns the file names below a certain @@ -730,6 +1160,9 @@ var _bintree = &bintree{nil, map[string]*bintree{ "bootstrap.min.css": &bintree{staticCssBootstrapMinCss, map[string]*bintree{}}, "bootstrap.min.css.br": &bintree{staticCssBootstrapMinCssBr, map[string]*bintree{}}, "bootstrap.min.css.gz": &bintree{staticCssBootstrapMinCssGz, map[string]*bintree{}}, + "player.css": &bintree{staticCssPlayerCss, map[string]*bintree{}}, + "player.css.br": &bintree{staticCssPlayerCssBr, map[string]*bintree{}}, + "player.css.gz": &bintree{staticCssPlayerCssGz, map[string]*bintree{}}, "style.css": &bintree{staticCssStyleCss, map[string]*bintree{}}, "style.css.br": &bintree{staticCssStyleCssBr, map[string]*bintree{}}, "style.css.gz": &bintree{staticCssStyleCssGz, map[string]*bintree{}}, @@ -745,11 +1178,18 @@ var _bintree = &bintree{nil, map[string]*bintree{ "jquery-3.5.1.slim.min.js": &bintree{staticJsJquery351SlimMinJs, map[string]*bintree{}}, "jquery-3.5.1.slim.min.js.br": &bintree{staticJsJquery351SlimMinJsBr, map[string]*bintree{}}, "jquery-3.5.1.slim.min.js.gz": &bintree{staticJsJquery351SlimMinJsGz, map[string]*bintree{}}, + "player.js": &bintree{staticJsPlayerJs, map[string]*bintree{}}, + "player.js.br": &bintree{staticJsPlayerJsBr, map[string]*bintree{}}, + "player.js.gz": &bintree{staticJsPlayerJsGz, map[string]*bintree{}}, }}, "radio52.png": &bintree{staticRadio52Png, map[string]*bintree{}}, + "speaker.svg": &bintree{staticSpeakerSvg, map[string]*bintree{}}, + "speaker.svg.br": &bintree{staticSpeakerSvgBr, map[string]*bintree{}}, + "speaker.svg.gz": &bintree{staticSpeakerSvgGz, map[string]*bintree{}}, }}, "templates": &bintree{nil, map[string]*bintree{ "index.html": &bintree{templatesIndexHtml, map[string]*bintree{}}, + "player.html": &bintree{templatesPlayerHtml, map[string]*bintree{}}, }}, }} diff --git a/node/debug.go b/node/debug.go index 0caf680374c12fca72a942c980ff6523e573f4d1..fe59f70c4dcd8e9208fdb09189737af88490a715 100644 --- a/node/debug.go +++ b/node/debug.go @@ -145,14 +145,14 @@ func (s *statusPageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { sort.Sort(statusList(statuses)) ms := mountsToStatus(s.n.mounts.GetMounts(), nodes, exemplary) - ctx := struct { + vars := struct { Domain string Nodes []*pb.Status Mounts []*mountStatus }{s.domain, statuses, ms} var buf bytes.Buffer - if err := tpl.ExecuteTemplate(&buf, "index.html", ctx); err != nil { + if err := tpl.ExecuteTemplate(&buf, "index.html", vars); err != nil { log.Printf("error rendering status page: %v", err) http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/node/http.go b/node/http.go index abac15cfadb53fd12b6efa286a7ec5e7f18a370f..53d36972b94ba0e609fed1c0b66b4f3230af88bb 100644 --- a/node/http.go +++ b/node/http.go @@ -4,6 +4,7 @@ package node //go:generate go-bindata --nocompress --pkg node static/... templates/... import ( + "bytes" "context" "crypto/tls" "flag" @@ -14,6 +15,7 @@ import ( "net/http" "net/http/httputil" "net/url" + "path/filepath" "strconv" "strings" "time" @@ -111,6 +113,12 @@ func newHTTPHandler(n *Node, icecastPort int, domain string) http.Handler { // statusHandler serves the home status page. statusHandler := gziphandler.GzipHandler(newStatusPageHandler(n, domain)) + // playerHandler serves the HTML audio player. + playerHandler := gziphandler.GzipHandler(withMount(n, func(m *pb.Mount, w http.ResponseWriter, r *http.Request) { + servePlayer(m, w, r, domain) + })) + mux.Handle("/player/", http.StripPrefix("/player", playerHandler)) + streamPrefixSlash := autoradio.IcecastMountPrefix + "/" mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { switch { @@ -196,6 +204,36 @@ func serveRedirect(lb *loadBalancer, mount *pb.Mount, w http.ResponseWriter, r * sendRedirect(w, r, targetURL.String()) } +func servePlayer(m *pb.Mount, w http.ResponseWriter, r *http.Request, domain string) { + // Build the stream URL using the incoming request. + streamURL := url.URL{ + Scheme: schemeFromRequest(r), + Host: r.Host, + Path: m.Path, + } + + // Make up the audio MIME type from the stream path. + mimeType := "audio/" + strings.TrimPrefix(filepath.Ext(m.Path), ".") + + vars := struct { + Domain string + Name string + Type string + URL string + }{domain, m.Path, mimeType, streamURL.String()} + + var buf bytes.Buffer + if err := tpl.ExecuteTemplate(&buf, "player.html", vars); err != nil { + log.Printf("error rendering player page: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("Content-Length", strconv.Itoa(buf.Len())) + addDefaultHeaders(w) + w.Write(buf.Bytes()) //nolint +} + // Serve a M3U response. This simply points back at the stream // redirect handler by dropping the .m3u suffix in the request URL. func sendM3U(w http.ResponseWriter, r *http.Request) { diff --git a/node/node_test.go b/node/node_test.go index 14ea7aed97144c18ef859980430cfb2a222a253d..23e963dda7f89314cb80ecfc6f36411b5df6205d 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -128,10 +128,21 @@ func TestNode_StatusPage(t *testing.T) { resp, err := http.Get(httpSrv.URL) if err != nil { - t.Fatalf("http.Get error: %v", err) + t.Fatalf("http.Get(/) error: %v", err) } - defer resp.Body.Close() + resp.Body.Close() if resp.StatusCode != 200 { - t.Fatalf("HTTP response: %s", resp.Status) + t.Fatalf("http.Get(/) error: HTTP: %s", resp.Status) } + + // Also check the player page. + resp, err = http.Get(httpSrv.URL + "/player/test.ogg") + if err != nil { + t.Fatalf("http.Get(/player/) error: %v", err) + } + resp.Body.Close() + if resp.StatusCode != 200 { + t.Fatalf("http.Get(/player/) error: HTTP: %s", resp.Status) + } + } diff --git a/node/static/css/player.css b/node/static/css/player.css new file mode 100644 index 0000000000000000000000000000000000000000..0551110ab1c75db9b8c892128d1447728ae8a2ba --- /dev/null +++ b/node/static/css/player.css @@ -0,0 +1,166 @@ +#player { + display: block; + position: absolute; + width: 375px; +} + +.button { + display: block; + width: 0; + height: 0; + border-top: 35px solid transparent; + border-bottom: 35px solid transparent; + border-left: 60px solid orangered; + margin: 70px auto; + position: relative; + z-index: 1; + transition: all .4; + -webkit-transition: all .4; + -moz-transition: all .4; + left: 10px; +} + +.button:before { + content: ''; + position: absolute; + top: -75px; + left: -115px; + bottom: -75px; + right: -35px; + border-radius: 50%; + border: 15px solid orangered; + z-index: 2; + transition: all .4s; + -webkit-transition: all .4; + -moz-transition: all .4; + transition: transform .3s; +} + +.loading:before { + animation: pulse .5s ease-in infinite; +} + +@keyframes pulse { + 0% { box-shadow: 0px 0px 0px red; } + 50% { box-shadow: 0px 0px 55px red; } +} + +.button:after { + content:''; + opacity:0; + transition: opacity .4s; +} + +.button:hover:before, +.button.play:before { + transform: scale(1.2); + -webkit-transform: scale(1.2); + -moz-transform: scale(1.2); +} + +.button.pause:after { + content: ''; + opacity: 1; + width: 50px; + height: 70px; + background: orangered; + position:absolute; + right: 1px; + top: -35px; + border-left: 25px solid orangered; + box-shadow: inset 25px 0 0 0 #fff; +} + + +/** VOLUME SLIDER **/ + +input[type=range] { + height: 58px; + -webkit-appearance: none; + margin: 5px; + width: 80%; + max-width: 260px; + background-color: transparent; +} +input[type=range]:focus { + outline: none; +} +input[type=range]::-webkit-slider-runnable-track { + width: 100%; + height: 6px; + cursor: pointer; + animate: 0.2s; + background: #FF0000; + border-radius: 3px; + border: 0px solid #F27B7F; +} +input[type=range]::-webkit-slider-thumb { + box-shadow: 0px 0px 0px #A6A6A6; + border: 2px solid #968994; + height: 50px; + width: 17px; + border-radius: 2px; + background: black; + cursor: pointer; + -webkit-appearance: none; + margin-top: -23px; +} +input[type=range]:focus::-webkit-slider-runnable-track { + background: #FF0000; +} +input[type=range]::-moz-range-track { + width: 100%; + height: 6px; + cursor: pointer; + animate: 0.2s; + box-shadow: 0px 0px 0px #A6A6A6; + background: #FF0000; + border-radius: 3px; + border: 0px solid #F27B7F; +} +input[type=range]::-moz-range-thumb { + box-shadow: 0px 0px 0px #A6A6A6; + border: 2px solid #968994; + height: 50px; + width: 17px; + border-radius: 2px; + background: black; + cursor: pointer; +} +input[type=range]::-ms-track { + width: 100%; + height: 6px; + cursor: pointer; + animate: 0.2s; + background: transparent; + border-color: transparent; + color: transparent; +} +input[type=range]::-ms-fill-lower { + background: #FF0000; + border: 0px solid #F27B7F; + border-radius: 6px; + box-shadow: 0px 0px 0px #A6A6A6; +} +input[type=range]::-ms-fill-upper { + background: #FF0000; + border: 0px solid #F27B7F; + border-radius: 6px; + box-shadow: 0px 0px 0px #A6A6A6; +} +input[type=range]::-ms-thumb { + margin-top: 1px; + box-shadow: 0px 0px 0px #A6A6A6; + border: 2px solid #968994; + height: 50px; + width: 17px; + border-radius: 2px; + background: black; + cursor: pointer; +} +input[type=range]:focus::-ms-fill-lower { + background: #FF0000; +} +input[type=range]:focus::-ms-fill-upper { + background: #FF0000; +} diff --git a/node/static/css/player.css.br b/node/static/css/player.css.br new file mode 100644 index 0000000000000000000000000000000000000000..b087a847e27c2250341aac5f49858b5b8008386d Binary files /dev/null and b/node/static/css/player.css.br differ diff --git a/node/static/css/player.css.gz b/node/static/css/player.css.gz new file mode 100644 index 0000000000000000000000000000000000000000..271e19a5f90adf1d4fb62271b8b45617ca1cde4f Binary files /dev/null and b/node/static/css/player.css.gz differ diff --git a/node/static/js/player.js b/node/static/js/player.js new file mode 100644 index 0000000000000000000000000000000000000000..e7a1015a21b84dd5039019b2b6fc9c0f34c9fe8a --- /dev/null +++ b/node/static/js/player.js @@ -0,0 +1,39 @@ +var spplayer = {}; + +spplayer.init = function(p) { + let status = "pause"; + const player = document.createElement("audio"); + const el_button = p.getElementsByClassName("button")[0]; + const el_volume = p.getElementsByClassName("volume")[0]; + + // Create a source with parameters extracted from the target + // element's attributes. + let source = document.createElement("source"); + source.type = p.getAttribute('stream-type'); + source.src = p.getAttribute('stream-src'); + player.appendChild(source); + + // Enough of the audio has loaded to allow playback to begin. + player.addEventListener("canplaythrough", function () { + el_button.classList.remove("loading"); + }); + + el_button.addEventListener("click", function () { + if (status === "play") { + player.load(); + } else { + player.play(); + } + status = status === "play" ? "pause" : "play"; + el_button.classList.toggle("pause"); + }); + + changeVolume = function (v) { + player.volume = el_volume.value/100; + }; + + el_volume.addEventListener("mousemove", changeVolume); + el_volume.addEventListener("change", changeVolume); +}; + +spplayer.init(document.getElementById("player")); diff --git a/node/static/js/player.js.br b/node/static/js/player.js.br new file mode 100644 index 0000000000000000000000000000000000000000..8a59ff471f7e9330971a311d76d5ee3b9fef10ee Binary files /dev/null and b/node/static/js/player.js.br differ diff --git a/node/static/js/player.js.gz b/node/static/js/player.js.gz new file mode 100644 index 0000000000000000000000000000000000000000..f470918478aa6f0b04723cf4ac1ddb5db1450d58 Binary files /dev/null and b/node/static/js/player.js.gz differ diff --git a/node/static/speaker.svg b/node/static/speaker.svg new file mode 100644 index 0000000000000000000000000000000000000000..7ef57b08f22a0aed7d5e68ee4879d8e4ac44a5b9 --- /dev/null +++ b/node/static/speaker.svg @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="500" height="500" viewBox="0 0 75 75"> +<path d="M39.389,13.769 L22.235,28.606 L6,28.606 L6,47.699 L21.989,47.699 L39.389,62.75 L39.389,13.769z" +style="stroke:#FF4500;stroke-width:5;stroke-linejoin:round;fill:#FF4500;" +/> +<path d="M48,27.6a19.5,19.5 0 0 1 0,21.4M55.1,20.5a30,30 0 0 1 0,35.6M61.6,14a38.8,38.8 0 0 1 0,48.6" style="fill:none;stroke:#FF4500;stroke-width:5;stroke-linecap:round"/> +</svg> diff --git a/node/static/speaker.svg.br b/node/static/speaker.svg.br new file mode 100644 index 0000000000000000000000000000000000000000..34863f793c5af6632d3a026af4ffb05566160c9b Binary files /dev/null and b/node/static/speaker.svg.br differ diff --git a/node/static/speaker.svg.gz b/node/static/speaker.svg.gz new file mode 100644 index 0000000000000000000000000000000000000000..5e4ec9e7eb815ab51da53d8f06f16b486df82cdd Binary files /dev/null and b/node/static/speaker.svg.gz differ diff --git a/node/templates/index.html b/node/templates/index.html index bb1a1b7d0be868785d9569c5065f51b6cccb4bff..1a470fcf051007773d5738a1b2f7c669fbece10e 100644 --- a/node/templates/index.html +++ b/node/templates/index.html @@ -25,26 +25,25 @@ <div class="col-lg-6"> <h4>Streams</h4> <ul> - {{$domain := .Domain}} {{range $m := .Mounts}} <li> - <a href="http://{{$domain}}{{$m.Mount.Path}}" + <a href="/player/{{$m.Mount.Path}}" {{if $m.Mount.RelayUrl}} data-toggle="tooltip" data-delay="300" title="relay of {{$m.Mount.RelayUrl}}" {{else if $m.IcecastMount.GetDescription}} data-toggle="tooltip" data-delay="300" title="{{$m.IcecastMount.GetDescription}}" {{end}} >{{$m.Mount.Path}}</a> - <a href="http://{{$domain}}{{$m.Mount.Path}}.m3u">(m3u)</a> + <a href="/{{$m.Mount.Path}}.m3u">(m3u)</a> <span class="badge badge-secondary">{{$m.Listeners}}</span> {{if $m.TransMounts}} <ul> {{range $tm := $m.TransMounts}} <li> - <a href="http://{{$domain}}{{$tm.Mount.Path}}" + <a href="/player/{{$tm.Mount.Path}}" data-toggle="tooltip" data-delay="300" title="{{$tm.Mount.TranscodeParams.String}}" >{{$tm.Mount.Path}}</a> - <a href="http://{{$domain}}{{$tm.Mount.Path}}.m3u">(m3u)</a> + <a href="/{{$tm.Mount.Path}}.m3u">(m3u)</a> <span class="badge badge-secondary">{{$tm.Listeners}}</span> </li> {{end}} diff --git a/node/templates/player.html b/node/templates/player.html new file mode 100644 index 0000000000000000000000000000000000000000..e2aa446b1a0d47a45d51dedb1c3f486be1c5c0b9 --- /dev/null +++ b/node/templates/player.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>{{.Domain}}</title> + <link rel="stylesheet" href="/static/css/bootstrap.min.css"> + <link rel="stylesheet" href="/static/css/style.css"> + <link rel="stylesheet" href="/static/css/player.css"> + <link rel="shortcut icon" href="/static/radio52.png"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + </head> + <body> + + <div class="container"> + <div class="page-header"> + <h1>{{.Name}}</h1> + </div> + + <div id="player" stream-type="{{.Type}}" stream-src="{{.URL}}"> + <a href="#" title="Listen" class="button loading"></a> + <div class="volumebar"> + <img src="/static/speaker.svg" width="60" height="60" alt="volume"> + <input class="volume" type="range" min="0" max="100" value="100" > + </div> + </div> + </div> + + <script type="text/javascript" src="/static/js/player.js"></script> + </body> +</html>