Skip to content
Snippets Groups Projects
Commit 401149e2 authored by ale's avatar ale
Browse files

Add an HTML audio player

parent cc772fcb
Branches
No related tags found
1 merge request!14Add an HTML audio player
Pipeline #11791 passed
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) \
......
......@@ -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(1612343832, 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(1612344201, 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(1612341006, 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{}},
}},
}}
 
......
......@@ -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
......
......@@ -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) {
......
#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;
}
File added
File added
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"));
File added
File added
<?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>
File added
File added
......@@ -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}}
......
<!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>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment