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

Add an HTML audio player

parent cc772fcb
No related branches found
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) \
......
This diff is collapsed.
......@@ -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