<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<title>SomaFM Music Player</title>
<link rel='stylesheet' href='https:////fonts.googleapis.com/css?family=Roboto+Condensed:700'>
<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css'>
<style>
*, *:before, *:after {
margin: 0;
padding: 0;
border: 0;
outline: none;
background-color: transparent;
text-transform: none;
text-shadow: none;
box-shadow: none;
box-sizing: border-box;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
-webkit-overflow-scrolling: touch;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transform-style: flat;
transition: border-color 400ms cubic-bezier(0.215, 0.61, 0.355, 1), background-color 400ms cubic-bezier(0.215, 0.61, 0.355, 1), opacity 400ms cubic-bezier(0.215, 0.61, 0.355, 1), transform 400ms cubic-bezier(0.215, 0.61, 0.355, 1);
}
article, aside, details, figcaption, figure, footer, header, hgroup,
menu, nav, section, main, summary, div, h1, h2, h3, h4, h5, h6, hr,
p, ol, ul, form, img {
display: block;
}
input, textarea, select, optgroup, option, button {
font-family: inherit;
font-size: inherit;
font-weight: normal;
line-height: inherit;
color: inherit;
}
select, button {
cursor: pointer;
}
a {
color: cornflowerblue;
}
a:hover {
color: #92b4f2;
}
hr {
display: block;
overflow: hidden;
margin: 1em 0;
height: 0;
border: 0;
border-bottom: 2px solid rgba(0, 0, 0, 0.08);
}
html, body {
display: block;
position: relative;
max-width: 100vw;
min-height: 100vh;
}
html {
overflow: hidden;
overflow-y: auto;
}
body {
font-family: "Roboto Condensed", sans-serif;
font-weight: 700;
font-size: calc( 20px - 6px );
line-height: 1.2em;
color: #787ba2;
background-size: cover;
background-color: #8086a0;
background-image: linear-gradient(217deg, rgba(220, 20, 60, 0.8), rgba(220, 20, 60, 0) 70.71%), linear-gradient(127deg, #8086a0, rgba(128, 134, 160, 0) 70.71%), linear-gradient(336deg, rgba(100, 149, 237, 0.8), rgba(100, 149, 237, 0) 70.71%);
}
@media only screen and (min-width : 420px) {
body {
font-size: calc( 20px - 4px );
}
}
@media only screen and (min-width : 720px) {
body {
font-size: calc( 20px - 2px );
}
}
@media only screen and (min-width : 1200px) {
body {
font-size: 20px;
}
}
.if-small {
display: none;
}
@media only screen and (min-width : 420px) {
.if-small {
display: initial;
}
}
.if-medium {
display: none;
}
@media only screen and (min-width : 720px) {
.if-medium {
display: initial;
}
}
.if-large {
display: none;
}
@media only screen and (min-width : 1200px) {
.if-large {
display: initial;
}
}
.hidden, [hidden], [v-cloak] {
display: none;
}
.disabled, [disabled] {
pointer-events: none;
opacity: 0.5;
}
.clickable {
cursor: pointer;
}
.card {
padding: 1em;
background-color: rgba(0, 0, 0, 0.08);
border-radius: 6px;
}
.push-top {
margin-top: 1em;
}
.push-right {
margin-right: 1em;
}
.push-bottom {
margin-bottom: 1em;
}
.push-left {
margin-left: 1em;
}
.push-all {
margin: 1em;
}
.pad-top {
padding-top: 1em;
}
.pad-right {
padding-right: 1em;
}
.pad-bottom {
padding-bottom: 1em;
}
.pad-left {
padding-left: 1em;
}
.pad-all {
padding: 1em;
}
.border-top {
border-top: 2px solid rgba(0, 0, 0, 0.08);
}
.border-right {
border-right: 2px solid rgba(0, 0, 0, 0.08);
}
.border-bottom {
border-bottom: 2px solid rgba(0, 0, 0, 0.08);
}
.border-left {
border-left: 2px solid rgba(0, 0, 0, 0.08);
}
.shadow-box {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
.shadow-text {
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
.fx {
position: relative;
-webkit-animation-direction: normal;
animation-direction: normal;
-webkit-animation-duration: 400ms;
animation-duration: 400ms;
-webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
-webkit-animation-iteration-count: 1;
animation-iteration-count: 1;
-webkit-animation-fill-mode: forwards;
animation-fill-mode: forwards;
}
.fx-notx {
transition: none !important;
}
.fx-ibk {
display: inline-block !important;
}
.fx-delay-1 {
-webkit-animation-delay: calc( calc( 400ms / 3 ) * 1 );
animation-delay: calc( calc( 400ms / 3 ) * 1 );
}
.fx-delay-2 {
-webkit-animation-delay: calc( calc( 400ms / 3 ) * 2 );
animation-delay: calc( calc( 400ms / 3 ) * 2 );
}
.fx-delay-3 {
-webkit-animation-delay: calc( calc( 400ms / 3 ) * 3 );
animation-delay: calc( calc( 400ms / 3 ) * 3 );
}
.fx-delay-4 {
-webkit-animation-delay: calc( calc( 400ms / 3 ) * 4 );
animation-delay: calc( calc( 400ms / 3 ) * 4 );
}
.fx-delay-5 {
-webkit-animation-delay: calc( calc( 400ms / 3 ) * 5 );
animation-delay: calc( calc( 400ms / 3 ) * 5 );
}
.fx-delay-6 {
-webkit-animation-delay: calc( calc( 400ms / 3 ) * 6 );
animation-delay: calc( calc( 400ms / 3 ) * 6 );
}
.fx-delay-7 {
-webkit-animation-delay: calc( calc( 400ms / 3 ) * 7 );
animation-delay: calc( calc( 400ms / 3 ) * 7 );
}
.fx-delay-8 {
-webkit-animation-delay: calc( calc( 400ms / 3 ) * 8 );
animation-delay: calc( calc( 400ms / 3 ) * 8 );
}
@-webkit-keyframes spinRight {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(359deg);
}
}
@keyframes spinRight {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(359deg);
}
}
.fx-spin-right {
-webkit-animation-name: spinRight;
animation-name: spinRight;
-webkit-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
@-webkit-keyframes spinLeft {
0% {
transform: rotate(359deg);
}
100% {
transform: rotate(0deg);
}
}
@keyframes spinLeft {
0% {
transform: rotate(359deg);
}
100% {
transform: rotate(0deg);
}
}
.fx-spin-left {
-webkit-animation-name: spinLeft;
animation-name: spinLeft;
-webkit-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
@-webkit-keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.fx-fade-in {
opacity: 0;
-webkit-animation-name: fadeIn;
animation-name: fadeIn;
}
@-webkit-keyframes fadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes fadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.fx-fade-out {
opacity: 1;
-webkit-animation-name: fadeOut;
animation-name: fadeOut;
}
@-webkit-keyframes dropIn {
0% {
opacity: 0;
transform: scale(1.4);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes dropIn {
0% {
opacity: 0;
transform: scale(1.4);
}
100% {
opacity: 1;
transform: scale(1);
}
}
.fx-drop-in {
opacity: 0;
transform: scale(1.4);
-webkit-animation-name: dropIn;
animation-name: dropIn;
}
@-webkit-keyframes zoomIn {
0% {
opacity: 0;
transform: scale(0.4);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes zoomIn {
0% {
opacity: 0;
transform: scale(0.4);
}
100% {
opacity: 1;
transform: scale(1);
}
}
.fx-zoom-in {
opacity: 0;
transform: scale(0.4);
-webkit-animation-name: zoomIn;
animation-name: zoomIn;
}
@-webkit-keyframes zoomOut {
0% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(0.4);
}
}
@keyframes zoomOut {
0% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(0.4);
}
}
.fx-zoom-out {
opacity: 1;
transform: scale(1);
-webkit-animation-name: zoomOut;
animation-name: zoomOut;
}
@-webkit-keyframes slideLeft {
0% {
opacity: 0;
transform: translateX(80px);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slideLeft {
0% {
opacity: 0;
transform: translateX(80px);
}
100% {
opacity: 1;
transform: translateX(0);
}
}
.fx-slide-left {
opacity: 0;
transform: translateX(80px);
-webkit-animation-name: slideLeft;
animation-name: slideLeft;
}
@-webkit-keyframes slideRight {
0% {
opacity: 0;
transform: translateX(calc( 0 - 80px ));
}
100% {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slideRight {
0% {
opacity: 0;
transform: translateX(calc( 0 - 80px ));
}
100% {
opacity: 1;
transform: translateX(0);
}
}
.fx-slide-right {
opacity: 0;
transform: translateX(calc( 0 - 80px ));
-webkit-animation-name: slideRight;
animation-name: slideRight;
}
@-webkit-keyframes slideUp {
0% {
opacity: 0;
transform: translateY(80px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideUp {
0% {
opacity: 0;
transform: translateY(80px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.fx-slide-up {
opacity: 0;
transform: translateY(80px);
-webkit-animation-name: slideUp;
animation-name: slideUp;
}
@-webkit-keyframes slideDown {
0% {
opacity: 0;
transform: translateY(calc( 0 - 80px ));
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideDown {
0% {
opacity: 0;
transform: translateY(calc( 0 - 80px ));
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.fx-slide-down {
opacity: 0;
transform: translateY(calc( 0 - 80px ));
-webkit-animation-name: slideDown;
animation-name: slideDown;
}
@-webkit-keyframes pulseFade {
0% {
opacity: 0.7;
}
50% {
opacity: 1;
}
100% {
opacity: 0.7;
}
}
@keyframes pulseFade {
0% {
opacity: 0.7;
}
50% {
opacity: 1;
}
100% {
opacity: 0.7;
}
}
.fx-pulse {
opacity: 0.7;
-webkit-animation-name: pulseFade;
animation-name: pulseFade;
-webkit-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.flex-row {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
}
.flex-wrap {
flex-wrap: wrap;
}
.flex-left {
justify-content: flex-start;
}
.flex-center {
justify-content: center;
}
.flex-right {
justify-content: flex-end;
}
.flex-space {
justify-content: space-between;
}
.flex-around {
justify-content: space-around;
}
.flex-stretch {
justify-content: stretch;
}
.flex-top {
align-items: flex-start;
}
.flex-middle {
align-items: center;
}
.flex-bottom {
align-items: flex-end;
}
.flex-half {
flex: 0.5;
}
.flex-1 {
flex: 1;
}
.flex-2 {
flex: 2;
}
.flex-3 {
flex: 3;
}
.flex-4 {
flex: 4;
}
.flex-5 {
flex: 5;
}
.flex-autorow {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
}
.flex-autorow > .flex-item {
flex: 1;
width: 100%;
margin: 0 0 1em 0;
}
.flex-autorow > .flex-item:last-of-type {
margin: 0;
}
@media only screen and (min-width : 720px) {
.flex-autorow {
flex-direction: row;
}
.flex-autorow > .flex-item {
margin: 0 1em 0 0;
}
.flex-autorow > .flex-item:last-of-type {
margin: 0;
}
}
.img-round {
overflow: hidden;
text-indent: -1000px;
border-radius: 1000px;
border: 2px solid whitesmoke;
background-color: #32334f;
background-image: linear-gradient(45deg, #32334f, #4f527e);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
.img-center {
display: block;
margin: 0 auto;
}
.common-btn {
display: inline-block;
text-align: center;
font-size: 180%;
font-weight: normal;
line-height: 1em;
width: 1em;
color: whitesmoke;
}
.common-btn:hover {
color: #c2c2c2;
}
.cta-btn {
display: inline-block;
text-decoration: none;
padding: 0.5em 1em;
color: #fac3ce;
background-color: #a41935;
border-radius: 100px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
line-height: 1.1em;
}
.cta-btn:hover {
color: #fcdae1;
background-color: #c51236;
}
.form-input {
display: flex;
flex: 1;
flex-direction: row;
align-items: center;
justify-content: stretch;
color: whitesmoke;
}
.form-input > input {
flex: 1;
line-height: 1.5em;
padding: 0 0.5em;
}
.form-slider {
display: flex;
position: relative;
flex-direction: row;
align-items: center;
justify-content: stretch;
width: 100%;
max-width: 6em;
line-height: 1em;
}
.form-slider > input {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
width: 100%;
margin: 0 0.5em;
}
.form-slider > input::-webkit-slider-runnable-track {
width: 100%;
height: 3px;
background-color: #32334f;
color: transparent !important;
border-color: transparent !important;
border-radius: 6px !important;
border: 0 !important;
}
.form-slider > input::-moz-range-track {
width: 100%;
height: 3px;
background-color: #32334f;
color: transparent !important;
border-color: transparent !important;
border-radius: 6px !important;
border: 0 !important;
}
.form-slider > input::-ms-track {
width: 100%;
height: 3px;
background-color: #32334f;
color: transparent !important;
border-color: transparent !important;
border-radius: 6px !important;
border: 0 !important;
}
.form-slider > input::-webkit-slider-thumb {
-webkit-appearance: none;
width: 1em;
height: 1em;
margin: -0.4em 0 0 0;
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
background-color: whitesmoke;
-webkit-transition: background 400ms cubic-bezier(0.215, 0.61, 0.355, 1);
transition: background 400ms cubic-bezier(0.215, 0.61, 0.355, 1);
color: transparent !important;
border-color: transparent !important;
border: 0 !important;
cursor: pointer;
}
.form-slider > input::-webkit-slider-thumb:hover {
background-color: #c2c2c2;
}
.form-slider > input::-moz-range-thumb {
width: 1em;
height: 1em;
margin: -0.4em 0 0 0;
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
background-color: whitesmoke;
-moz-transition: background 400ms cubic-bezier(0.215, 0.61, 0.355, 1);
transition: background 400ms cubic-bezier(0.215, 0.61, 0.355, 1);
color: transparent !important;
border-color: transparent !important;
border: 0 !important;
cursor: pointer;
}
.form-slider > input::-moz-range-thumb:hover {
background-color: #c2c2c2;
}
.form-slider > input::-ms-thumb {
width: 1em;
height: 1em;
margin: -0.4em 0 0 0;
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
background-color: whitesmoke;
-ms-transition: background 400ms cubic-bezier(0.215, 0.61, 0.355, 1);
transition: background 400ms cubic-bezier(0.215, 0.61, 0.355, 1);
color: transparent !important;
border-color: transparent !important;
border: 0 !important;
cursor: pointer;
}
.form-slider > input::-ms-thumb:hover {
background-color: #c2c2c2;
}
@-webkit-keyframes popoverShow {
0% {
transform: translateX(-50%) scale(0.8);
opacity: 0;
}
35% {
transform: translateX(-50%) scale(1.2);
opacity: 0.8;
}
100% {
transform: translateX(-50%) scale(1);
opacity: 1;
}
}
@keyframes popoverShow {
0% {
transform: translateX(-50%) scale(0.8);
opacity: 0;
}
35% {
transform: translateX(-50%) scale(1.2);
opacity: 0.8;
}
100% {
transform: translateX(-50%) scale(1);
opacity: 1;
}
}
.popover {
position: relative;
}
.popover .popover-box {
display: none;
position: absolute;
padding: 0.5em 0;
max-width: 300px;
min-height: 100px;
left: 50%;
bottom: 50%;
transition: none;
transform: translateX(-50%);
background-color: #2e2f49;
border-radius: 6px;
box-shadow: 0 1px 20px rgba(0, 0, 0, 0.6);
-webkit-animation: popoverShow 400ms cubic-bezier(0.215, 0.61, 0.355, 1) forwards;
animation: popoverShow 400ms cubic-bezier(0.215, 0.61, 0.355, 1) forwards;
z-index: 2000;
}
.popover .popover-box:before {
content: "";
display: none;
position: absolute;
transition: none;
width: 0;
height: 0;
transform: translateX(-50%);
left: 50%;
z-index: 2001;
}
.popover .popover-box > button {
display: block;
width: 100%;
text-align: left;
padding: 0.5em 1em;
line-height: 1.2em;
white-space: nowrap;
background-color: rgba(30, 31, 48, 0);
}
.popover .popover-box > button:hover {
background-color: rgba(30, 31, 48, 0.2);
}
.popover .popover-box > button + button {
border-top: 2px solid rgba(0, 0, 0, 0.08);
}
.popover .popover-box.popover-left {
transform: none;
left: auto;
right: 0;
}
.popover .popover-box.popover-right {
transform: none;
left: 0;
right: auto;
}
.popover .popover-box.popover-top {
top: auto;
bottom: 100%;
}
.popover .popover-box.popover-top:before {
display: block;
top: auto;
bottom: -10px;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid #2e2f49;
}
.popover .popover-box.popover-bottom {
top: 100%;
bottom: auto;
}
.popover .popover-box.popover-bottom:before {
display: block;
top: -10px;
bottom: auto;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-bottom: 10px solid #2e2f49;
}
.popover:hover > .popover-box, .popover:active > .popover-box {
display: block;
}
h1, h2, h3, h4, h5, h6 {
display: block;
font-weight: normal;
line-height: 1.1em;
color: whitesmoke;
}
h1 {
font-size: 220%;
}
h2 {
font-size: 200%;
}
h3 {
font-size: 180%;
}
h4 {
font-size: 160%;
}
h5 {
font-size: 140%;
}
h6 {
font-size: 120%;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.text-center {
text-align: center;
}
.text-justify {
text-align: justify;
}
.text-uppercase {
text-transform: uppercase;
}
.text-lowercase {
text-transform: lowercase;
}
.text-capitalize {
text-transform: capitalize;
}
.text-underline {
text-decoration: underline;
}
.text-striked {
text-decoration: line-through;
}
.text-italic {
font-style: italic;
}
.text-bold {
font-weight: bold;
}
.text-nowrap {
white-space: nowrap;
}
.text-clip {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.text-primary {
color: crimson;
}
.text-secondary {
color: cornflowerblue;
}
.text-grey {
color: slategray;
}
.text-bright {
color: whitesmoke;
}
.text-faded {
opacity: 0.5;
}
.text-big {
font-size: 120%;
}
.text-bigger {
font-size: 180%;
}
.text-huge {
font-size: 240%;
}
.text-small {
font-size: 90%;
}
.text-condense {
letter-spacing: -1px;
}
.app-wrap {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex-wrap: nowrap;
min-height: 100vh;
width: 100%;
}
.player-wrap {
display: block;
overflow: hidden;
position: relative;
flex: 1;
width: 100%;
height: 100vh;
background-color: #1e1f30;
}
.player-wrap > .player-bg, .player-wrap > .player-canvas {
display: block;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.player-wrap > .player-bg {
background-image: url("https://raw.githubusercontent.com/rainner/soma-fm-player/master/public/img/bg.jpg");
background-position: bottom right;
background-repeat: no-repeat;
background-size: cover;
opacity: 0.4;
}
@media only screen and (min-width : 720px) {
.player-wrap {
margin: 0 2em;
max-width: 1080px;
height: calc( 100vh - ( 1em * 4 ) );
max-height: 700px;
border-radius: 6px;
box-shadow: 0 1px 30px rgba(0, 0, 0, 0.8);
}
}
.player-layout {
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: stretch;
height: 100%;
}
.player-layout .player-header,
.player-layout .player-content,
.player-layout .player-footer {
position: relative;
}
.player-layout .player-header,
.player-layout .player-footer {
padding: 0 1em;
height: 3.5em;
min-height: 3.5em;
background-color: rgba(0, 0, 0, 0.08);
}
.player-layout .player-header > h2 {
color: crimson;
}
.player-layout .player-header > h2 i {
vertical-align: bottom;
}
.player-layout .player-content {
flex: 1;
height: 100%;
overflow: hidden;
overflow-y: auto;
padding: 1em;
}
.player-layout .player-content > section {
margin: auto 0;
}
@media only screen and (min-width : 720px) {
.player-layout .player-content {
padding: 1em 2em;
}
}
.player-greet {
flex: 1;
}
@media only screen and (min-width : 720px) {
.player-greet {
flex: 0.5;
}
}
.player-tracklist {
display: block;
position: relative;
list-style: none;
}
.player-tracklist > li + li {
margin-top: 0.5em;
}
.player-controls {
position: relative;
}
.player-stations {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0);
pointer-events: none;
z-index: 1;
}
.player-stations .player-stations-sidebar {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: stretch;
position: absolute;
top: 0;
right: -320px;
width: 320px;
min-height: 100%;
max-height: 100%;
background-color: #222336;
}
@media only screen and (min-width : 420px) {
.player-stations .player-stations-sidebar {
right: -420px;
width: 420px;
}
}
.player-stations .player-stations-sidebar .player-stations-header,
.player-stations .player-stations-sidebar .player-stations-footer {
padding: 0 1em;
min-height: 3.5em;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
}
.player-stations .player-stations-sidebar .player-stations-list {
display: block;
list-style: none;
overflow: hidden;
overflow-y: auto;
margin-left: -10px;
padding-left: 10px;
flex: 1;
}
.player-stations .player-stations-sidebar .player-stations-list .player-stations-list-item {
position: relative;
padding: 1em;
background-color: rgba(0, 0, 0, 0.1);
cursor: pointer;
}
.player-stations .player-stations-sidebar .player-stations-list .player-stations-list-item:nth-child(odd) {
background-color: rgba(0, 0, 0, 0.18);
}
.player-stations .player-stations-sidebar .player-stations-list .player-stations-list-item:hover {
background-color: rgba(0, 0, 0, 0);
}
.player-stations .player-stations-sidebar .player-stations-list .player-stations-list-item.active {
background-color: #1a1b2a;
}
.player-stations .player-stations-sidebar .player-stations-list .player-stations-list-item.active h6 {
color: crimson;
}
.player-stations.visible {
background-color: rgba(0, 0, 0, 0.4);
pointer-events: auto;
z-index: 1000;
}
.player-stations.visible .player-stations-sidebar {
transform: translateX(-320px);
box-shadow: 0 1px 20px rgba(0, 0, 0, 0.6);
}
@media only screen and (min-width : 420px) {
.player-stations.visible .player-stations-sidebar {
transform: translateX(-420px);
}
}
.player-stations.visible .player-stations-list-item.active:before {
content: "";
display: block;
position: absolute;
transition: none;
transform: translateY(-50%);
top: 50%;
left: -10px;
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
border-right: 10px solid #1a1b2a;
}
</style>
<script>
window.console = window.console || function(t) {};
</script>
</head>
<body translate="no">
<!-- app root container -->
<div class="app-wrap" id="app" v-cloak>
<!-- app player container -->
<main class="player-wrap fx fx-fade-in" ref="playerWrap" style="opacity: 0">
<!-- bg absolute elements -->
<figure class="player-bg" ref="playerBg"></figure>
<canvas class="player-canvas" ref="playerCanvas"></canvas>
<!-- main player layout -->
<section class="player-layout">
<!-- player top header -->
<header class="player-header flex-row flex-middle flex-stretch">
<h2 class="text-clip flex-1"><i class="fa fa-headphones"></i> <span>Soma FM Player</span></h2>
<button class="text-nowrap common-btn" @click="toggleSidebar( true )"><i class="fa fa-bars"></i></button>
</header>
<!-- player middle content area -->
<main class="player-content flex-row">
<!-- default greet message -->
<section class="player-greet" v-if="!hasChannel && !hasErrors">
<div class="fx fx-slide-left push-bottom"><h1>Pick a Station</h1></div>
<div class="fx fx-slide-left fx-delay-1 push-bottom">This is a music streaming player for the channels provided by SomaFM.com. Just pick a station from the sidebar to the right to start listening.</div>
<div class="fx fx-slide-up fx-delay-2 pad-top"><button class="cta-btn" @click="toggleSidebar( true )"><i class="fa fa-headphones"> </i> View Stations</button></div>
</section>
<!-- show selected channel info if possible -->
<section class="player-channel flex-1" v-if="hasChannel && !hasErrors" :key="channel.id">
<div class="flex-autorow flex-middle flex-stretch">
<!-- station details -->
<div class="flex-item flex-1">
<!-- station -->
<div class="push-bottom pad-bottom border-bottom">
<div class="flex-row flex-middle">
<img class="img-round fx fx-drop-in fx-delay-1" :src="channel.largeimage" width="80" height="80" :alt="channel.title" />
<div class="pad-left fx fx-slide-left fx-delay-2">
<div class="text-clip text-uppercase">{{ channel.genre | toSpaces }}</div>
<h2 class="text-clip">{{ channel.title }}</h2>
</div>
</div>
</div>
<!-- description -->
<div class="push-bottom pad-bottom border-bottom fx fx-slide-up fx-delay-3">
{{ channel.description }}
</div>
<!-- current track -->
<div class="push-bottom pad-bottom border-bottom fx fx-slide-up fx-delay-4" :key="track.date">
<div><span class="text-faded">DJ:</span> <span class="text-default">{{ channel.dj | toText( 'N/A' ) }}</span></div>
<div><span class="text-faded">Playing:</span> <span class="text-secondary">{{ track.title | toText( 'N/A' ) }}</span></div>
<div><span class="text-faded">From:</span> <span class="text-bright">{{ track.album | toText( 'N/A' ) }}</span></div>
<div><span class="text-faded">By:</span> <span class="text-default">{{ track.artist | toText( 'N/A' ) }}</span></div>
</div>
<!-- buttons -->
<div class="push-bottom">
<a class="cta-btn text-nowrap fx fx-slide-up fx-delay-5" :href="channel.twitter" title="Open link" target="_blank">
<i class="fa fa-twitter"></i> Twitter
</a>
<a class="cta-btn text-nowrap fx fx-slide-up fx-delay-6" :href="channel.infourl" title="Channel page" target="_blank">
<span class="fx fx-notx fx-ibk fx-drop-in fx-delay-1" :key="channel.listeners"><i class="fa fa-headphones"></i> {{ channel.listeners | toCommas( 0 ) }}</span>
</a>
<a class="cta-btn text-nowrap fx fx-slide-up fx-delay-7" :href="channel.plsfile" title="Download PLS" target="_blank">
<i class="fa fa-download"></i>
</a>
</div>
</div>
<!-- songs list -->
<div class="flex-item flex-1">
<div class="push-bottom">
<h5 class="fx fx-slide-left fx-delay-1">Recent Tracks</h5>
</div>
<div class="card push-bottom" v-if="!hasSongs">
There are no songs loaded yet for this station.
</div>
<ul class="player-tracklist push-bottom" v-if="hasSongs">
<li v-for="( s, i ) of songsList" :key="s.date" class="card fx" :class="'fx-slide-left fx-delay-' + ( i + 2 )">
<div><span class="text-secondary">{{ s.title | toText( 'N/A' ) }}</span></div>
<div><span class="text-faded">From:</span> <span class="text-bright">{{ s.album | toText( 'N/A' ) }}</span></div>
<div><span class="text-faded">By:</span> <span class="text-default">{{ s.artist | toText( 'N/A' ) }}</span></div>
</li>
</ul>
</div>
</div>
</section>
<!-- show tracks for selected channel if possible -->
<section class="player-errors flex-1 text-center" v-if="hasErrors" key="errors">
<div class="push-bottom fx fx-drop-in fx-delay-1">
<i class="fa fa-plug text-huge text-faded"></i>
</div>
<div class="push-bottom fx fx-slide-up fx-delay-2">
<h3>Oops, there's a problem!</h3>
</div>
<hr />
<div class="text-primary push-bottom fx fx-slide-up fx-delay-3" v-if="errors.init" v-text="errors.init"></div>
<div class="text-primary push-bottom fx fx-slide-up fx-delay-4" v-if="errors.stream" v-text="errors.stream"></div>
<hr />
<button class="cta-btn text-nowrap fx fx-slide-up fx-delay-5" @click="tryAgain">
<i class="fa fa-refresh"></i> Try again
</button>
</section>
</main>
<!-- player footer with controls -->
<footer class="player-footer flex-row flex-middle flex-space">
<!-- player controls -->
<section class="player-controls flex-row flex-middle push-right" :class="{ 'disabled': !canPlay }">
<button class="common-btn" @click="togglePlay()">
<i v-if="playing" class="fa fa-stop fx fx-drop-in" key="stop"></i>
<i v-else class="fa fa-play fx fx-drop-in" key="play"></i>
</button>
<div class="form-slider push-left">
<i class="fa fa-volume-down"></i>
<input class="common-slider" type="range" min="0.0" max="1.0" step="0.1" value="0.5" v-model="volume" />
<i class="fa fa-volume-up"></i>
</div>
<div class="text-clip push-left">
<span>{{ timeDisplay }}</span>
<span class="fx fx-fade-in fx-delay-1" v-if="hasChannel" :key="channel.id"> | {{ channel.title }}</span>
</div>
</section>
<!-- player links -->
<section class="player-links text-nowrap">
<a class="common-btn text-faded" href="" title="View on Github" target="_blank">
<i class="fa fa-github"></i>
</a>
<a class="common-btn text-faded" href="" title="Codepen Projects" target="_blank">
<i class="fa fa-codepen"></i>
</a>
</section>
</footer>
</section> <!-- layout wrapper -->
<!-- player stations overlay + sidebar -->
<section class="player-stations" :class="{ 'visible': sidebar }" @click="toggleSidebar( false )">
<aside class="player-stations-sidebar" @click.stop>
<!-- sidebar search -->
<header class="player-stations-header flex-row flex-middle flex-stretch">
<div class="form-input push-right">
<i class="fa fa-search"></i>
<input type="text" placeholder="Search station..." v-model="searchText" />
</div>
<button class="common-btn" @click="toggleSidebar( false )"><i class="fa fa-times-circle"></i></button>
</header>
<!-- sidebar stations list -->
<ul class="player-stations-list">
<li class="player-stations-list-item flex-row flex-top flex-stretch" v-for="c of channelsList" :key="c.id" @click="selectChannel( c )" :class="{ 'active': c.active }">
<figure class="push-right if-small">
<img class="img-round" width="70" height="70" :src="c.largeimage" :alt="c.title" />
</figure>
<aside class="flex-1">
<div class="flex-row flex-middle flex-space">
<h6 class="text-bright text-clip">{{ c.title }}</h6>
<div class="text-secondary"><i class="fa fa-headphones"></i> {{ c.listeners | toCommas( 0 ) }}</div>
</div>
<div class="text-small">
<span class="text-faded text-uppercase text-small">{{ c.genre | toSpaces }}</span> <br />
{{ c.description }}
</div>
</aside>
</li>
</ul>
<!-- sidebar sort options -->
<footer class="player-stations-footer flex-row flex-middle flex-stretch">
<div class="flex-1 push-right">
<span @click="toggleSortOrder()" class="fa clickable" :class="{ 'fa-sort-amount-desc': sortOrder === 'desc', 'fa-sort-amount-asc': sortOrder === 'asc' }"> </span>
<span class="text-faded">Sort: </span>
<span class="text-secondary popover">
<span class="clickable">{{ sortLabel }}</span>
<span class="popover-box popover-top">
<button @click="sortBy( 'title', 'asc' )">Station Name</button>
<button @click="sortBy( 'listeners', 'desc' )">Listeners Count</button>
<button @click="sortBy( 'genre', 'asc' )">Music Genre</button>
</span>
</span>
</div>
<div> </div>
</footer>
</aside>
</section>
</main> <!-- player -->
</div> <!-- wrapper -->
<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/96/three.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js'></script>
<script id="rendered-js" >
/**
* Soma FM Web Player
* Author:
* Site: /
*/
// .andAHHAbnn.
// .aAHHHAAUUAAHHHAn.
// dHP^~" "~^THb.
// . .AHF YHA. .
// | .AHHb. .dHHA. |
// | HHAUAAHAbn adAHAAUAHA |
// I HF~"_____ ____ ]HHH I
// HHI HAPK""~^YUHb dAHHHHHHHHHH IHH
// HHI HHHD> .andHH HHUUP^~YHHHH IHH
// YUI ]HHP "~Y P~" THH[ IUP
// " `HK ]HH' "
// THAn. .d.aAAn.b. .dHHP
// ]HHHHAAUP" ~~ "YUAAHHHH[
// `HHP^~" .annn. "~^YHH'
// YHb ~" "" "~ dHF
// "YAb..abdHHbndbndAP"
// THHAAb. .adAHHF
// "UHHHHHHHHHHU"
// ]HHUUHHHHHH[
// .adHHb "HHHHHbn.
// ..andAAHHHHHHb.AHHHHHHHAAbnn..
// .ndAAHHHHHHUUHHHHHHHHHHUP^~"~^YUHHHAAbn.
// "~^YUHHP" "~^YUHHUP" "^YUP^"
// "" "~~"
/**
* Sphere object
*/
const Sphere = {
group: null,
shapes: [],
move: new THREE.Vector3(0, 0, 0),
ease: 8,
create(box, scene) {
this.group = new THREE.Object3D();
let shape1 = new THREE.CircleGeometry(1, 10);
let shape2 = new THREE.CircleGeometry(2, 20);
let points = new THREE.SphereGeometry(100, 30, 14).vertices;
let material = new THREE.MeshLambertMaterial({ color: 0xffffff, opacity: 0, side: THREE.DoubleSide });
let center = new THREE.Vector3(0, 0, 0);
let radius = 12;
for (let i = 0; i < points.length; i++) {
let { x, y, z } = points[i];
let home = { x, y, z };
let cycle = THREE.Math.randInt(0, 100);
let pace = THREE.Math.randInt(10, 30);
let shape = new THREE.Mesh(i % 2 ? shape1 : shape2, material);
shape.position.set(x, y, z);
shape.lookAt(center);
shape.userData = { radius, cycle, pace, home };
this.group.add(shape);
}
this.group.position.set(500, 0, 0);
this.group.rotation.x = Math.PI / 2 + .6;
scene.add(this.group);
},
update(box, mouse, freq) {
let bass = Math.floor(freq[1] | 0) / 255;
this.move.x = box.width * .06 + -(mouse.x * 0.02);
this.group.position.x += (this.move.x - this.group.position.x) / this.ease;
this.group.position.y += (this.move.y - this.group.position.y) / this.ease;
this.group.position.z = 10 + bass * 80;
this.group.rotation.y -= 0.003;
for (let i = 0; i < this.group.children.length; i++) {
let shape = this.group.children[i];
let { radius, cycle, pace, home } = shape.userData;
shape.position.set(home.x, home.y, home.z);
shape.translateZ(bass * Math.sin(cycle / pace) * radius);
shape.userData.cycle++;
}
} };
/**
* Vue filters
*/
Vue.filter('toCommas', (num, decimals) => {
let o = { style: 'decimal', minimumFractionDigits: decimals, maximumFractionDigits: decimals };
return new Intl.NumberFormat('en-US', o).format(num);
});
Vue.filter('toSpaces', str => {
return String(str || '').trim().replace(/[^\w\`\'\-]+/g, ' ').trim();
});
Vue.filter('toText', (str, def) => {
str = String(str || '').replace(/[^\w\`\'\-\.\!\?]+/g, ' ').trim();
return str || String(def || '');
});
/**
* Vue app
*/
new Vue({
el: '#app',
data: {
// toggles
init: false,
playing: false,
loading: false,
sidebar: false,
// channels stuff
channels: [], // all channels
channel: {}, // selected channel
songs: [], // recent tracks
track: {}, // current track
errors: {}, // error messages
// animation stuff
fxBox: null,
fxRenderer: null,
fxScene: null,
fxColor: null,
fxLight: null,
fxCamera: null,
fxMouse: { x: 0, y: 0 },
fxObjects: [],
// audio stuff
audio: new Audio(),
context: new AudioContext(),
freqData: new Uint8Array(),
audioSrc: null,
audioGain: null,
analyser: null,
volume: 0.5,
// timer stuff
timeStart: 0,
timeDisplay: '00:00:00',
timeItv: null,
// sorting stuff
searchText: '',
sortParam: 'listeners',
sortOrder: 'desc',
// timer stuff
anf: null,
sto: null,
itv: null },
// watch methods
watch: {
// when app is ready
init() {
setTimeout(this.setupCanvas, 100);
setTimeout(this.initSidebar, 500);
},
// watch playing status
playing() {
if (this.playing) {this.startClock();} else
{this.stopClock();}
},
// update player volume
volume() {
this.setVolume(this.volume);
} },
// computed methods
computed: {
// filter channels list
channelsList() {
let list = this.channels.slice();
let search = this.searchText.replace(/[^\w\s\-]+/g, '').replace(/[\r\s\t\n]+/g, ' ').trim();
if (search && search.length > 1) {
let reg = new RegExp('^(' + search + ')', 'i');
list = list.filter(i => reg.test(i.title + ' ' + i.description));
}
if (this.sortParam) {
list = this.sortList(list, this.sortParam, this.sortOrder);
}
if (this.channel.id) {
list = list.map(i => {
i.active = this.channel.id === i.id ? true : false;
return i;
});
}
return list;
},
// filter songs list
songsList() {
let list = this.songs.slice();
return list;
},
// sort-by label for buttons, etc
sortLabel() {
switch (this.sortParam) {
case 'title':return 'Station Name';
case 'listeners':return 'Listeners Count';
case 'genre':return 'Music Genre';}
},
// check if audio can be played
canPlay() {
return this.channel.id && !this.loading ? true : false;
},
// check if a channel is selected
hasChannel() {
return this.channel.id ? true : false;
},
// check if there are tracks loaded
hasSongs() {
return this.songs.length ? true : false;
},
// check if there are errors to show
hasErrors() {
return this.checkError('init') || this.checkError('stream') ? true : false;
} },
// custom methods
methods: {
// set an erro message
setError(key, err) {
let errors = Object.assign({}, this.errors);
errors[key] = String(err || '').trim();
if (err) console.warn('ERROR(' + key + '):', err);
this.errors = errors;
this.init = true;
},
// check if an error has been set for a key
checkError(key) {
return key && this.errors.hasOwnProperty(key) && this.errors[key];
},
// clear all error messages
clearErrors() {
Object.keys(this.errors).forEach(key => {
this.errors[key] = '';
});
},
// reset selected channel
resetPlayer() {
this.channel = {};
this.songs = [];
this.clearErrors();
this.getChannels(true);
},
// try resuming stream problem if possible
tryAgain() {
if (this.checkError('init')) return this.resetPlayer();
if (this.channel.id) return this.playChannel(this.channel);
},
// show/hide the sidebar
toggleSidebar(toggle) {
this.sidebar = typeof toggle === 'boolean' ? toggle : false;
},
// show sidebar at startup if there are no errors
initSidebar() {
if (this.checkError('init')) return;
this.toggleSidebar(true);
},
// toggle stream playback for current selected channel
togglePlay() {
if (this.loading) return;
if (this.playing) return this.closeAudio();
if (this.channel.id) return this.playChannel(this.channel);
},
// toggle sort order
toggleSortOrder() {
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc';
},
// apply sorting and toggle order
sortBy(param, order) {
if (this.sortParam === param) {this.toggleSortOrder();} else
{this.sortOrder = order || 'asc';}
this.sortParam = param;
},
// sort an array by key and order
sortList(list, param, order) {
return list.sort((a, b) => {
if (a.hasOwnProperty(param) && b.hasOwnProperty(param)) {
let _a = a[param];
let _b = b[param];
_a = typeof _a === 'string' ? _a.toUpperCase() : _a;
_b = typeof _b === 'string' ? _b.toUpperCase() : _b;
if (order === 'asc') {
if (_a < _b) return -1;
if (_a > _b) return 1;
}
if (order === 'desc') {
if (_a > _b) return -1;
if (_a < _b) return 1;
}
}
return 0;
});
},
// get channels data from api
getChannels(sidebar) {
let endpoint = 'https://somafm.com/channels.json';
let emsg = ['There was a problem trying to load the list of available channels from SomaFM.'];
axios.get(endpoint).then(res => {
if (!res || !res.data || !res.data.channels) {
emsg.push('The API response did not have any channels data available at this time.');
emsg.push('Status: Channels API Error.');
return this.setError('channels', emsg.join(' '));
}
for (let c of res.data.channels) {
if (!Array.isArray(c.playlists)) continue;
// filter and sanitize list of channels
c.twitter = c.twitter ? 'https://twitter.com/@' + c.twitter : ''; // full twitter url
c.plsfile = c.playlists.filter(p => p.format === 'mp3' && /^(highest|high)$/.test(p.quality)).shift().url || '';
c.mp3file = 'http://ice1.somafm.com/' + c.id + '-128-mp3'; // assumed stream url
c.songsurl = 'https://somafm.com/songs/' + c.id + '.json'; // songs data url
c.infourl = 'https://somafm.com/' + c.id + '/'; // channel page url
c.listeners = c.listeners | 0; // force numeric
c.updated = c.updated | 0; // force numeric
c.active = false; // select state
// update selected channel
if (this.isCurrentChannel(c)) {
c.active = true;
this.channel = Object.assign(this.channel, c);
}
}
this.channels = res.data.channels.slice();
if (sidebar) this.toggleSidebar(true);
this.setError('init', '');
this.setError('channels', '');
}).
catch(e => {
emsg.push('Try again, or check your internet connection.');
emsg.push('Status: ' + String(e.message || 'Channels API Error') + '.');
let errstr = emsg.join(' ');
if (!this.channels.length) this.setError('init', errstr);
this.setError('channels', errstr);
});
},
// fetch songs for a channel
fetchSongs(channel, cb) {
if (!channel || !channel.id || !channel.songsurl) return;
if (!this.isCurrentChannel(channel)) {this.songs = [];this.track = {};}
let emsg = ['There was a problem trying to load the list of songs for channel ' + channel.title + ' from SomaFM.'];
axios.get(channel.songsurl).then(res => {
if (!res || !res.data || !res.data.songs) {
emsg.push('The API response did not have any songs data available at this time.');
emsg.push('Status: Songs API Error.');
return this.setError('songs', emsg.join(' '));
}
let songs = res.data.songs.slice();
this.track = songs.shift();
this.songs = songs.slice(0, 3);
this.setError('songs', '');
if (typeof cb === 'function') cb(songs);
}).
catch(e => {
emsg.push('Try again, or check your internet connection.');
emsg.push('Status: ' + String(e.message || 'Songs API Error') + '.');
this.setError('songs', emsg.join(' '));
});
},
// run maintenance tasks on a timer
setupMaintenance() {
this.itv = setInterval(() => {
this.getChannels(); // update channels
this.fetchSongs(this.channel); // update channel tracks
// ...
}, 1000 * 30);
},
// setup animation canvas
setupCanvas() {
if (!this.$refs.playerWrap) return;
if (!this.$refs.playerCanvas) return;
// default canvas and player dimensions
const player = this.$refs.playerWrap;
const canvas = this.$refs.playerCanvas;
// setup THREE renderer and replace default canvas
this.fxBox = player.getBoundingClientRect();
this.fxScene = new THREE.Scene();
this.fxRenderer = new THREE.WebGLRenderer({ alpha: true, antialias: true, precision: 'highp' });
this.fxRenderer.setClearColor(0x000000, 0);
this.fxRenderer.setPixelRatio(window.devicePixelRatio);
this.fxRenderer.domElement.className = canvas.className;
// setup camera
this.fxCamera = new THREE.PerspectiveCamera(60, this.fxBox.width / this.fxBox.height, 0.1, 20000);
this.fxCamera.lookAt(this.fxScene.position);
this.fxCamera.position.set(0, 0, 300);
this.fxCamera.rotation.set(0, 0, 0);
// light color
this.fxColor = new THREE.Color();
this.fxColor.setHSL(this.fxHue, 1, .5);
// setup light source
this.fxLight = new THREE.PointLight(0xffffff, 4, 400);
this.fxLight.position.set(0, 0, 420);
this.fxLight.castShadow = false;
this.fxLight.target = this.fxScene;
this.fxLight.color = this.fxColor;
this.fxScene.add(this.fxLight);
// setup canvas and events
canvas.parentNode.replaceChild(this.fxRenderer.domElement, canvas);
window.addEventListener('mousemove', this.updateMousePosition);
window.addEventListener('resize', this.updateStageSize);
// add objects
this.fxObjects.push(Sphere);
// setup objects and start animation
for (let o of this.fxObjects) o.create(this.fxBox, this.fxScene);
this.updateStageSize();
this.updateAnimations();
},
// update mouse position from center of canvas
updateMousePosition(e) {
if (!this.fxBox || !e) return;
this.fxMouse.x = Math.max(0, e.pageX || e.clientX || 0) - (this.fxBox.left + this.fxBox.width / 2);
this.fxMouse.y = Math.max(0, e.pageY || e.clientY || 0) - (this.fxBox.top + this.fxBox.height / 2);
},
// update canvas size
updateStageSize() {
if (!this.$refs.playerWrap || !this.fxRenderer) return;
this.fxBox = this.$refs.playerWrap.getBoundingClientRect();
this.fxCamera.aspect = this.fxBox.width / this.fxBox.height;
this.fxCamera.updateProjectionMatrix();
this.fxRenderer.setSize(this.fxBox.width, this.fxBox.height);
},
// update light color based on audio freq
updateStageLight() {
let dist = Math.floor(this.freqData[1] | 0) / 255;
let color = Math.floor(this.freqData[16] | 0) / 255;
this.fxLight.distance = 360 + 140 * dist;
this.fxColor.setHSL(color, .5, .5);
},
// update custom objects in 3d scene
updateSceneObjects() {
for (let o of this.fxObjects) {
o.update(this.fxBox, this.fxMouse, this.freqData);
}
},
// audio visualizer animation loop
updateAnimations() {
this.anf = requestAnimationFrame(this.updateAnimations);
if (!this.fxRenderer || !this.fxCamera || !this.analyser || !this.freqData) return;
this.analyser.getByteFrequencyData(this.freqData);
this.updateSceneObjects();
this.updateStageLight();
this.fxRenderer.render(this.fxScene, this.fxCamera);
},
// setup audio routing and stream events
setupAudio() {
// setup audio sources
this.audioSrc = this.context.createMediaElementSource(this.audio);
this.audioGain = this.context.createGain();
this.analyser = this.context.createAnalyser();
// connect sources
this.audioSrc.connect(this.audioGain);
this.audioSrc.connect(this.analyser);
this.audioGain.connect(this.context.destination);
this.setVolume(this.volume);
// check when stream can start playing
this.audio.addEventListener('canplay', e => {
this.audio.play();
this.freqData = new Uint8Array(this.analyser.frequencyBinCount);
});
// check if stream is buffering
this.audio.addEventListener('waiting', e => {
this.playing = false;
this.loading = true;
});
// check if stream is done buffering
this.audio.addEventListener('playing', e => {
this.setError('stream', '');
this.playing = true;
this.loading = false;
});
// check if stream has ended
this.audio.addEventListener('ended', e => {
this.playing = false;
this.loading = false;
});
// check for steam error
this.audio.addEventListener('error', e => {
let emsg = [];
emsg.push('The selected audio stream could not load, or has stopped loading.');
emsg.push('Try again, or check your internet connection.');
emsg.push('Status: ' + String(e.message || 'Stream URL Error') + '.');
this.setError('stream', emsg.join(' '));
this.playing = false;
this.loading = false;
});
},
// set audio volume
setVolume(volume) {
if (!this.audioGain) return;
volume = parseFloat(volume) || 0;
volume = volume < 0 ? 0 : volume;
volume = volume > 1 ? 1 : volume;
this.audioGain.gain.value = volume;
},
// checks is a channel is currently selected
isCurrentChannel(channel) {
if (!channel || !channel.id || !this.channel.id) return false;
if (this.channel.id !== channel.id) return false;
return true;
},
// play audio stream for a channel
playChannel(channel) {
if (this.playing) return;
this.clearErrors();
this.audio.src = channel.mp3file + '/?x=' + Date.now();
this.audio.crossOrigin = 'anonymous';
this.audio.load();
},
// select a channel to play
selectChannel(channel) {
if (!channel || !channel.id) return;
if (this.isCurrentChannel(channel)) return;
this.closeAudio();
this.toggleSidebar(false);
this.playChannel(channel);
this.fetchSongs(channel);
this.channel = channel;
},
// close active audio
closeAudio() {
this.setError('stream', '');
try {this.audio.pause();} catch (e) {}
try {this.audio.stop();} catch (e) {}
try {this.audio.close();} catch (e) {}
this.playing = false;
},
// start tracking playback time
startClock() {
this.stopClock();
this.timeStart = Date.now();
this.timeItv = setInterval(this.updateClock, 1000);
this.updateClock();
},
// update tracking playback time
updateClock() {
let p = n => n < 10 ? '0' + n : '' + n;
let elapsed = (Date.now() - this.timeStart) / 1000;
let seconds = Math.floor(elapsed % 60);
let minutes = Math.floor(elapsed / 60 % 60);
let hours = Math.floor(elapsed / 3600);
this.timeDisplay = p(hours) + ':' + p(minutes) + ':' + p(seconds);
},
// stop tracking playback time
stopClock() {
if (this.timeItv) clearInterval(this.timeItv);
this.timeItv = null;
},
// clear timer refs
clearTimers() {
if (this.sto) clearTimeout(this.sto);
if (this.itv) clearInterval(this.itv);
if (this.anf) cancelAnimationFrame(this.anf);
} },
// on app mounted
mounted() {
this.getChannels();
this.setupAudio();
this.setupMaintenance();
},
// on app destroyed
destroyed() {
this.closeAudio();
this.clearTimers();
} });
</script>
</body>
</html>
Comments
Post a Comment