mirror of
https://github.com/badaix/snapcast.git
synced 2025-08-04 00:59:32 +02:00
Update snapweb to v0.4.0
This commit is contained in:
parent
4a7b9b66ee
commit
72434eef10
6 changed files with 412 additions and 114 deletions
BIN
server/etc/snapweb/10-seconds-of-silence.mp3
Normal file
BIN
server/etc/snapweb/10-seconds-of-silence.mp3
Normal file
Binary file not shown.
|
@ -4,7 +4,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#455A64">
|
||||
<meta name="author" content="Johannes M. Pohl">
|
||||
<meta name="version" content="0.3.0">
|
||||
<meta name="version" content="0.4.0">
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
|
|
BIN
server/etc/snapweb/snapcast-512.png
Normal file
BIN
server/etc/snapweb/snapcast-512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
|
@ -1,11 +1,6 @@
|
|||
"use strict";
|
||||
class Host {
|
||||
constructor(json) {
|
||||
this.arch = "";
|
||||
this.ip = "";
|
||||
this.mac = "";
|
||||
this.name = "";
|
||||
this.os = "";
|
||||
this.fromJson(json);
|
||||
}
|
||||
fromJson(json) {
|
||||
|
@ -15,11 +10,14 @@ class Host {
|
|||
this.name = json.name;
|
||||
this.os = json.os;
|
||||
}
|
||||
arch = "";
|
||||
ip = "";
|
||||
mac = "";
|
||||
name = "";
|
||||
os = "";
|
||||
}
|
||||
class Client {
|
||||
constructor(json) {
|
||||
this.id = "";
|
||||
this.connected = false;
|
||||
this.fromJson(json);
|
||||
}
|
||||
fromJson(json) {
|
||||
|
@ -32,14 +30,15 @@ class Client {
|
|||
this.lastSeen = { sec: json.lastSeen.sec, usec: json.lastSeen.usec };
|
||||
this.connected = Boolean(json.connected);
|
||||
}
|
||||
id = "";
|
||||
host;
|
||||
snapclient;
|
||||
config;
|
||||
lastSeen;
|
||||
connected = false;
|
||||
}
|
||||
class Group {
|
||||
constructor(json) {
|
||||
this.name = "";
|
||||
this.id = "";
|
||||
this.stream_id = "";
|
||||
this.muted = false;
|
||||
this.clients = [];
|
||||
this.fromJson(json);
|
||||
}
|
||||
fromJson(json) {
|
||||
|
@ -50,6 +49,11 @@ class Group {
|
|||
for (let client of json.clients)
|
||||
this.clients.push(new Client(client));
|
||||
}
|
||||
name = "";
|
||||
id = "";
|
||||
stream_id = "";
|
||||
muted = false;
|
||||
clients = [];
|
||||
getClient(id) {
|
||||
for (let client of this.clients) {
|
||||
if (client.id == id)
|
||||
|
@ -58,23 +62,88 @@ class Group {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
class Metadata {
|
||||
constructor(json) {
|
||||
this.fromJson(json);
|
||||
}
|
||||
fromJson(json) {
|
||||
this.title = json.title;
|
||||
this.artist = json.artist;
|
||||
this.album = json.album;
|
||||
this.artUrl = json.artUrl;
|
||||
this.duration = json.duration;
|
||||
}
|
||||
title;
|
||||
artist;
|
||||
album;
|
||||
artUrl;
|
||||
duration;
|
||||
}
|
||||
class Properties {
|
||||
constructor(json) {
|
||||
this.fromJson(json);
|
||||
}
|
||||
fromJson(json) {
|
||||
this.loopStatus = json.loopStatus;
|
||||
this.shuffle = json.shuffle;
|
||||
this.volume = json.volume;
|
||||
this.rate = json.rate;
|
||||
this.playbackStatus = json.playbackStatus;
|
||||
this.position = json.position;
|
||||
this.minimumRate = json.minimumRate;
|
||||
this.maximumRate = json.maximumRate;
|
||||
this.canGoNext = Boolean(json.canGoNext);
|
||||
this.canGoPrevious = Boolean(json.canGoPrevious);
|
||||
this.canPlay = Boolean(json.canPlay);
|
||||
this.canPause = Boolean(json.canPause);
|
||||
this.canSeek = Boolean(json.canSeek);
|
||||
this.canControl = Boolean(json.canControl);
|
||||
if (json.metadata != undefined) {
|
||||
this.metadata = new Metadata(json.metadata);
|
||||
}
|
||||
else {
|
||||
this.metadata = new Metadata({});
|
||||
}
|
||||
}
|
||||
loopStatus;
|
||||
shuffle;
|
||||
volume;
|
||||
rate;
|
||||
playbackStatus;
|
||||
position;
|
||||
minimumRate;
|
||||
maximumRate;
|
||||
canGoNext = false;
|
||||
canGoPrevious = false;
|
||||
canPlay = false;
|
||||
canPause = false;
|
||||
canSeek = false;
|
||||
canControl = false;
|
||||
metadata;
|
||||
}
|
||||
class Stream {
|
||||
constructor(json) {
|
||||
this.id = "";
|
||||
this.status = "";
|
||||
this.fromJson(json);
|
||||
}
|
||||
fromJson(json) {
|
||||
this.id = json.id;
|
||||
this.status = json.status;
|
||||
if (json.properties != undefined) {
|
||||
this.properties = new Properties(json.properties);
|
||||
}
|
||||
else {
|
||||
this.properties = new Properties({});
|
||||
}
|
||||
let juri = json.uri;
|
||||
this.uri = { raw: juri.raw, scheme: juri.scheme, host: juri.host, path: juri.path, fragment: juri.fragment, query: juri.query };
|
||||
}
|
||||
id = "";
|
||||
status = "";
|
||||
uri;
|
||||
properties;
|
||||
}
|
||||
class Server {
|
||||
constructor(json) {
|
||||
this.groups = [];
|
||||
this.streams = [];
|
||||
if (json)
|
||||
this.fromJson(json);
|
||||
}
|
||||
|
@ -89,6 +158,9 @@ class Server {
|
|||
this.streams.push(new Stream(jstream));
|
||||
}
|
||||
}
|
||||
groups = [];
|
||||
server;
|
||||
streams = [];
|
||||
getClient(id) {
|
||||
for (let group of this.groups) {
|
||||
let client = group.getClient(id);
|
||||
|
@ -130,39 +202,186 @@ class SnapControl {
|
|||
setTimeout(() => this.connect(), 1000);
|
||||
};
|
||||
}
|
||||
action(answer) {
|
||||
switch (answer.method) {
|
||||
onNotification(notification) {
|
||||
let stream;
|
||||
switch (notification.method) {
|
||||
case 'Client.OnVolumeChanged':
|
||||
let client = this.getClient(answer.params.id);
|
||||
client.config.volume = answer.params.volume;
|
||||
let client = this.getClient(notification.params.id);
|
||||
client.config.volume = notification.params.volume;
|
||||
updateGroupVolume(this.getGroupFromClient(client.id));
|
||||
break;
|
||||
return true;
|
||||
case 'Client.OnLatencyChanged':
|
||||
this.getClient(answer.params.id).config.latency = answer.params.latency;
|
||||
break;
|
||||
this.getClient(notification.params.id).config.latency = notification.params.latency;
|
||||
return false;
|
||||
case 'Client.OnNameChanged':
|
||||
this.getClient(answer.params.id).config.name = answer.params.name;
|
||||
break;
|
||||
this.getClient(notification.params.id).config.name = notification.params.name;
|
||||
return true;
|
||||
case 'Client.OnConnect':
|
||||
case 'Client.OnDisconnect':
|
||||
this.getClient(answer.params.client.id).fromJson(answer.params.client);
|
||||
break;
|
||||
this.getClient(notification.params.client.id).fromJson(notification.params.client);
|
||||
return true;
|
||||
case 'Group.OnMute':
|
||||
this.getGroup(answer.params.id).muted = Boolean(answer.params.mute);
|
||||
break;
|
||||
this.getGroup(notification.params.id).muted = Boolean(notification.params.mute);
|
||||
return true;
|
||||
case 'Group.OnStreamChanged':
|
||||
this.getGroup(answer.params.id).stream_id = answer.params.stream_id;
|
||||
break;
|
||||
this.getGroup(notification.params.id).stream_id = notification.params.stream_id;
|
||||
this.updateProperties(notification.params.stream_id);
|
||||
return true;
|
||||
case 'Stream.OnUpdate':
|
||||
this.getStream(answer.params.id).fromJson(answer.params.stream);
|
||||
break;
|
||||
stream = this.getStream(notification.params.id);
|
||||
stream.fromJson(notification.params.stream);
|
||||
this.updateProperties(stream.id);
|
||||
return true;
|
||||
case 'Server.OnUpdate':
|
||||
this.server.fromJson(answer.params.server);
|
||||
break;
|
||||
this.server.fromJson(notification.params.server);
|
||||
this.updateProperties(this.getMyStreamId());
|
||||
return true;
|
||||
case 'Stream.OnProperties':
|
||||
stream = this.getStream(notification.params.id);
|
||||
stream.properties.fromJson(notification.params.properties);
|
||||
if (this.getMyStreamId() == stream.id)
|
||||
this.updateProperties(stream.id);
|
||||
return false;
|
||||
default:
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
updateProperties(stream_id) {
|
||||
if (!('mediaSession' in navigator)) {
|
||||
console.log('updateProperties: mediaSession not supported');
|
||||
return;
|
||||
}
|
||||
if (stream_id != this.getMyStreamId()) {
|
||||
console.log('updateProperties: not my stream id: ' + stream_id + ', mine: ' + this.getMyStreamId());
|
||||
return;
|
||||
}
|
||||
let props;
|
||||
let metadata;
|
||||
try {
|
||||
props = this.getStreamFromClient(SnapStream.getClientId()).properties;
|
||||
metadata = this.getStreamFromClient(SnapStream.getClientId()).properties.metadata;
|
||||
}
|
||||
catch (e) {
|
||||
console.log('updateProperties failed: ' + e);
|
||||
return;
|
||||
}
|
||||
// https://developers.google.com/web/updates/2017/02/media-session
|
||||
// https://github.com/googlechrome/samples/tree/gh-pages/media-session
|
||||
// https://googlechrome.github.io/samples/media-session/audio.html
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/MediaSession/setActionHandler#seekto
|
||||
console.log('updateProperties: ', props);
|
||||
let play_state = "none";
|
||||
if (props.playbackStatus != undefined) {
|
||||
if (props.playbackStatus == "playing") {
|
||||
audio.play();
|
||||
play_state = "playing";
|
||||
}
|
||||
else if (props.playbackStatus == "paused") {
|
||||
audio.pause();
|
||||
play_state = "paused";
|
||||
}
|
||||
else if (props.playbackStatus == "stopped") {
|
||||
audio.pause();
|
||||
play_state = "none";
|
||||
}
|
||||
}
|
||||
let mediaSession = navigator.mediaSession;
|
||||
mediaSession.playbackState = play_state;
|
||||
console.log('updateProperties playbackState: ', navigator.mediaSession.playbackState);
|
||||
// if (props.canGoNext == undefined || !props.canGoNext!)
|
||||
mediaSession.setActionHandler('play', () => {
|
||||
props.canPlay ?
|
||||
this.sendRequest('Stream.Control', { id: stream_id, command: 'play' }) : null;
|
||||
});
|
||||
mediaSession.setActionHandler('pause', () => {
|
||||
props.canPause ?
|
||||
this.sendRequest('Stream.Control', { id: stream_id, command: 'pause' }) : null;
|
||||
});
|
||||
mediaSession.setActionHandler('previoustrack', () => {
|
||||
props.canGoPrevious ?
|
||||
this.sendRequest('Stream.Control', { id: stream_id, command: 'previous' }) : null;
|
||||
});
|
||||
mediaSession.setActionHandler('nexttrack', () => {
|
||||
props.canGoNext ?
|
||||
this.sendRequest('Stream.Control', { id: stream_id, command: 'next' }) : null;
|
||||
});
|
||||
try {
|
||||
mediaSession.setActionHandler('stop', () => {
|
||||
props.canControl ?
|
||||
this.sendRequest('Stream.Control', { id: stream_id, command: 'stop' }) : null;
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.log('Warning! The "stop" media session action is not supported.');
|
||||
}
|
||||
let defaultSkipTime = 10; // Time to skip in seconds by default
|
||||
mediaSession.setActionHandler('seekbackward', (event) => {
|
||||
let offset = (event.seekOffset || defaultSkipTime) * -1;
|
||||
if (props.position != undefined)
|
||||
Math.max(props.position + offset, 0);
|
||||
props.canSeek ?
|
||||
this.sendRequest('Stream.Control', { id: stream_id, command: 'seek', params: { 'offset': offset } }) : null;
|
||||
});
|
||||
mediaSession.setActionHandler('seekforward', (event) => {
|
||||
let offset = event.seekOffset || defaultSkipTime;
|
||||
if ((metadata.duration != undefined) && (props.position != undefined))
|
||||
Math.min(props.position + offset, metadata.duration);
|
||||
props.canSeek ?
|
||||
this.sendRequest('Stream.Control', { id: stream_id, command: 'seek', params: { 'offset': offset } }) : null;
|
||||
});
|
||||
try {
|
||||
mediaSession.setActionHandler('seekto', (event) => {
|
||||
let position = event.seekTime || 0;
|
||||
if (metadata.duration != undefined)
|
||||
Math.min(position, metadata.duration);
|
||||
props.canSeek ?
|
||||
this.sendRequest('Stream.Control', { id: stream_id, command: 'setPosition', params: { 'position': position } }) : null;
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.log('Warning! The "seekto" media session action is not supported.');
|
||||
}
|
||||
if ((metadata.duration != undefined) && (props.position != undefined) && (props.position <= metadata.duration)) {
|
||||
if ('setPositionState' in mediaSession) {
|
||||
console.log('Updating position state: ' + props.position + '/' + metadata.duration);
|
||||
mediaSession.setPositionState({
|
||||
duration: metadata.duration,
|
||||
playbackRate: 1.0,
|
||||
position: props.position
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
mediaSession.setPositionState({
|
||||
duration: 0,
|
||||
playbackRate: 1.0,
|
||||
position: 0
|
||||
});
|
||||
}
|
||||
console.log('updateMetadata: ', metadata);
|
||||
// https://github.com/Microsoft/TypeScript/issues/19473
|
||||
let title = metadata.title || "Unknown Title";
|
||||
let artist = (metadata.artist != undefined) ? metadata.artist[0] : "Unknown Artist";
|
||||
let album = metadata.album || "";
|
||||
let artwork = metadata.artUrl || 'snapcast-512.png';
|
||||
console.log('Metadata title: ' + title + ', artist: ' + artist + ', album: ' + album + ", artwork: " + artwork);
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: title,
|
||||
artist: artist,
|
||||
album: album,
|
||||
artwork: [
|
||||
// { src: artwork, sizes: '250x250', type: 'image/jpeg' },
|
||||
// 'https://dummyimage.com/96x96', sizes: '96x96', type: 'image/png' },
|
||||
{ src: artwork, sizes: '128x128', type: 'image/png' },
|
||||
{ src: artwork, sizes: '192x192', type: 'image/png' },
|
||||
{ src: artwork, sizes: '256x256', type: 'image/png' },
|
||||
{ src: artwork, sizes: '384x384', type: 'image/png' },
|
||||
{ src: artwork, sizes: '512x512', type: 'image/png' },
|
||||
]
|
||||
});
|
||||
// mediaSession.setActionHandler('seekbackward', function () { });
|
||||
// mediaSession.setActionHandler('seekforward', function () { });
|
||||
}
|
||||
getClient(client_id) {
|
||||
let client = this.server.getClient(client_id);
|
||||
if (client == null) {
|
||||
|
@ -199,6 +418,19 @@ class SnapControl {
|
|||
return group;
|
||||
throw new Error(`group for client ${client_id} was null`);
|
||||
}
|
||||
getStreamFromClient(client_id) {
|
||||
let group = this.getGroupFromClient(client_id);
|
||||
return this.getStream(group.stream_id);
|
||||
}
|
||||
getMyStreamId() {
|
||||
try {
|
||||
let group = this.getGroupFromClient(SnapStream.getClientId());
|
||||
return this.getStream(group.stream_id).id;
|
||||
}
|
||||
catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
getStream(stream_id) {
|
||||
let stream = this.server.getStream(stream_id);
|
||||
if (stream == null) {
|
||||
|
@ -248,6 +480,7 @@ class SnapControl {
|
|||
}
|
||||
setStream(group_id, stream_id) {
|
||||
this.getGroup(group_id).stream_id = stream_id;
|
||||
this.updateProperties(stream_id);
|
||||
this.sendRequest('Group.SetStream', { id: group_id, stream_id: stream_id });
|
||||
}
|
||||
setClients(group_id, clients) {
|
||||
|
@ -271,34 +504,43 @@ class SnapControl {
|
|||
return this.msg_id;
|
||||
}
|
||||
onMessage(msg) {
|
||||
let answer = JSON.parse(msg);
|
||||
let is_response = (answer.id != undefined);
|
||||
console.log("Received " + (is_response ? "response" : "notification") + ", json: " + JSON.stringify(answer));
|
||||
let json_msg = JSON.parse(msg);
|
||||
let is_response = (json_msg.id != undefined);
|
||||
console.log("Received " + (is_response ? "response" : "notification") + ", json: " + JSON.stringify(json_msg));
|
||||
if (is_response) {
|
||||
if (answer.id == this.status_req_id) {
|
||||
this.server = new Server(answer.result.server);
|
||||
if (json_msg.id == this.status_req_id) {
|
||||
this.server = new Server(json_msg.result.server);
|
||||
this.updateProperties(this.getMyStreamId());
|
||||
show();
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (Array.isArray(answer)) {
|
||||
for (let a of answer) {
|
||||
this.action(a);
|
||||
let refresh = false;
|
||||
if (Array.isArray(json_msg)) {
|
||||
for (let notification of json_msg) {
|
||||
refresh = this.onNotification(notification) || refresh;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.action(answer);
|
||||
refresh = this.onNotification(json_msg);
|
||||
}
|
||||
// TODO: don't update everything, but only the changed,
|
||||
// e.g. update the values for the volume sliders
|
||||
show();
|
||||
if (refresh)
|
||||
show();
|
||||
}
|
||||
}
|
||||
baseUrl;
|
||||
connection;
|
||||
server;
|
||||
msg_id;
|
||||
status_req_id;
|
||||
}
|
||||
let snapcontrol;
|
||||
let snapstream = null;
|
||||
let hide_offline = true;
|
||||
let autoplay_done = false;
|
||||
let audio = document.createElement('audio');
|
||||
function autoplayRequested() {
|
||||
return document.location.hash.match(/autoplay/) !== null;
|
||||
}
|
||||
|
@ -368,17 +610,21 @@ function show() {
|
|||
// Group mute and refresh button
|
||||
content += "<div class='groupheader'>";
|
||||
content += streamselect;
|
||||
// let cover_img: string = server.getStream(group.stream_id)!.properties.metadata.artUrl || "snapcast-512.png";
|
||||
// content += "<img src='" + cover_img + "' class='cover-img' id='cover_" + group.id + "'>";
|
||||
let clientCount = 0;
|
||||
for (let client of group.clients)
|
||||
if (!hide_offline || client.connected)
|
||||
clientCount++;
|
||||
if (clientCount > 1) {
|
||||
let volume = snapcontrol.getGroupVolume(group, hide_offline);
|
||||
// content += "<div class='client'>";
|
||||
content += "<a href=\"javascript:setMuteGroup('" + group.id + "'," + !muted + ");\"><img src='" + mute_img + "' class='mute-button'></a>";
|
||||
content += "<div class='slidergroupdiv'>";
|
||||
content += " <input type='range' draggable='false' min=0 max=100 step=1 id='vol_" + group.id + "' oninput='javascript:setGroupVolume(\"" + group.id + "\")' value=" + volume + " class='slider'>";
|
||||
// content += " <input type='range' min=0 max=100 step=1 id='vol_" + group.id + "' oninput='javascript:setVolume(\"" + client.id + "\"," + client.config.volume.muted + ")' value=" + client.config.volume.percent + " class='" + sliderclass + "'>";
|
||||
content += "</div>";
|
||||
// content += "</div>";
|
||||
}
|
||||
// transparent placeholder edit icon
|
||||
content += "<div class='edit-group-icon'>✎</div>";
|
||||
|
@ -531,11 +777,20 @@ function play() {
|
|||
if (snapstream) {
|
||||
snapstream.stop();
|
||||
snapstream = null;
|
||||
audio.pause();
|
||||
audio.src = '';
|
||||
document.body.removeChild(audio);
|
||||
}
|
||||
else {
|
||||
snapstream = new SnapStream(config.baseUrl);
|
||||
// User interacted with the page. Let's play audio...
|
||||
document.body.appendChild(audio);
|
||||
audio.src = "10-seconds-of-silence.mp3";
|
||||
audio.loop = true;
|
||||
audio.play().then(() => {
|
||||
snapcontrol.updateProperties(snapcontrol.getMyStreamId());
|
||||
});
|
||||
}
|
||||
show();
|
||||
}
|
||||
function setMuteGroup(id, mute) {
|
||||
snapcontrol.muteGroup(id, mute);
|
||||
|
|
|
@ -41,8 +41,6 @@ function uuidv4() {
|
|||
}
|
||||
class Tv {
|
||||
constructor(sec, usec) {
|
||||
this.sec = 0;
|
||||
this.usec = 0;
|
||||
this.sec = sec;
|
||||
this.usec = usec;
|
||||
}
|
||||
|
@ -53,15 +51,11 @@ class Tv {
|
|||
getMilliseconds() {
|
||||
return this.sec * 1000 + this.usec / 1000;
|
||||
}
|
||||
sec = 0;
|
||||
usec = 0;
|
||||
}
|
||||
class BaseMessage {
|
||||
constructor(_buffer) {
|
||||
this.type = 0;
|
||||
this.id = 0;
|
||||
this.refersTo = 0;
|
||||
this.received = new Tv(0, 0);
|
||||
this.sent = new Tv(0, 0);
|
||||
this.size = 0;
|
||||
}
|
||||
deserialize(buffer) {
|
||||
let view = new DataView(buffer);
|
||||
|
@ -89,11 +83,16 @@ class BaseMessage {
|
|||
getSize() {
|
||||
return 0;
|
||||
}
|
||||
type = 0;
|
||||
id = 0;
|
||||
refersTo = 0;
|
||||
received = new Tv(0, 0);
|
||||
sent = new Tv(0, 0);
|
||||
size = 0;
|
||||
}
|
||||
class CodecMessage extends BaseMessage {
|
||||
constructor(buffer) {
|
||||
super(buffer);
|
||||
this.codec = "";
|
||||
this.payload = new ArrayBuffer(0);
|
||||
if (buffer) {
|
||||
this.deserialize(buffer);
|
||||
|
@ -111,11 +110,12 @@ class CodecMessage extends BaseMessage {
|
|||
this.payload = buffer.slice(34 + codecSize, 34 + codecSize + payloadSize);
|
||||
console.log("payload: " + this.payload);
|
||||
}
|
||||
codec = "";
|
||||
payload;
|
||||
}
|
||||
class TimeMessage extends BaseMessage {
|
||||
constructor(buffer) {
|
||||
super(buffer);
|
||||
this.latency = new Tv(0, 0);
|
||||
if (buffer) {
|
||||
this.deserialize(buffer);
|
||||
}
|
||||
|
@ -136,6 +136,7 @@ class TimeMessage extends BaseMessage {
|
|||
getSize() {
|
||||
return 8;
|
||||
}
|
||||
latency = new Tv(0, 0);
|
||||
}
|
||||
class JsonMessage extends BaseMessage {
|
||||
constructor(buffer) {
|
||||
|
@ -168,19 +169,11 @@ class JsonMessage extends BaseMessage {
|
|||
return encoded.length + 4;
|
||||
// return JSON.stringify(this.json).length;
|
||||
}
|
||||
json;
|
||||
}
|
||||
class HelloMessage extends JsonMessage {
|
||||
constructor(buffer) {
|
||||
super(buffer);
|
||||
this.mac = "";
|
||||
this.hostname = "";
|
||||
this.version = "0.1.0";
|
||||
this.clientName = "Snapweb";
|
||||
this.os = "";
|
||||
this.arch = "web";
|
||||
this.instance = 1;
|
||||
this.uniqueId = "";
|
||||
this.snapStreamProtocolVersion = 2;
|
||||
if (buffer) {
|
||||
this.deserialize(buffer);
|
||||
}
|
||||
|
@ -202,14 +195,19 @@ class HelloMessage extends JsonMessage {
|
|||
this.json = { "MAC": this.mac, "HostName": this.hostname, "Version": this.version, "ClientName": this.clientName, "OS": this.os, "Arch": this.arch, "Instance": this.instance, "ID": this.uniqueId, "SnapStreamProtocolVersion": this.snapStreamProtocolVersion };
|
||||
return super.serialize();
|
||||
}
|
||||
mac = "";
|
||||
hostname = "";
|
||||
version = "0.0.0";
|
||||
clientName = "Snapweb";
|
||||
os = "";
|
||||
arch = "web";
|
||||
instance = 1;
|
||||
uniqueId = "";
|
||||
snapStreamProtocolVersion = 2;
|
||||
}
|
||||
class ServerSettingsMessage extends JsonMessage {
|
||||
constructor(buffer) {
|
||||
super(buffer);
|
||||
this.bufferMs = 0;
|
||||
this.latency = 0;
|
||||
this.volumePercent = 0;
|
||||
this.muted = false;
|
||||
if (buffer) {
|
||||
this.deserialize(buffer);
|
||||
}
|
||||
|
@ -226,14 +224,14 @@ class ServerSettingsMessage extends JsonMessage {
|
|||
this.json = { "bufferMs": this.bufferMs, "latency": this.latency, "volume": this.volumePercent, "muted": this.muted };
|
||||
return super.serialize();
|
||||
}
|
||||
bufferMs = 0;
|
||||
latency = 0;
|
||||
volumePercent = 0;
|
||||
muted = false;
|
||||
}
|
||||
class PcmChunkMessage extends BaseMessage {
|
||||
constructor(buffer, sampleFormat) {
|
||||
super(buffer);
|
||||
this.timestamp = new Tv(0, 0);
|
||||
// payloadSize: number = 0;
|
||||
this.payload = new ArrayBuffer(0);
|
||||
this.idx = 0;
|
||||
this.deserialize(buffer);
|
||||
this.sampleFormat = sampleFormat;
|
||||
this.type = 2;
|
||||
|
@ -288,27 +286,22 @@ class PcmChunkMessage extends BaseMessage {
|
|||
}
|
||||
this.payload = payload;
|
||||
}
|
||||
timestamp = new Tv(0, 0);
|
||||
// payloadSize: number = 0;
|
||||
payload = new ArrayBuffer(0);
|
||||
idx = 0;
|
||||
sampleFormat;
|
||||
}
|
||||
class AudioStream {
|
||||
timeProvider;
|
||||
sampleFormat;
|
||||
bufferMs;
|
||||
constructor(timeProvider, sampleFormat, bufferMs) {
|
||||
this.timeProvider = timeProvider;
|
||||
this.sampleFormat = sampleFormat;
|
||||
this.bufferMs = bufferMs;
|
||||
this.chunks = new Array();
|
||||
// setRealSampleRate(sampleRate: number) {
|
||||
// if (sampleRate == this.sampleFormat.rate) {
|
||||
// this.correctAfterXFrames = 0;
|
||||
// }
|
||||
// else {
|
||||
// this.correctAfterXFrames = Math.ceil((this.sampleFormat.rate / sampleRate) / (this.sampleFormat.rate / sampleRate - 1.));
|
||||
// console.debug("setRealSampleRate: " + sampleRate + ", correct after X: " + this.correctAfterXFrames);
|
||||
// }
|
||||
// }
|
||||
this.chunk = undefined;
|
||||
this.volume = 1;
|
||||
this.muted = false;
|
||||
this.lastLog = 0;
|
||||
}
|
||||
chunks = new Array();
|
||||
setVolume(percent, muted) {
|
||||
// let base = 10;
|
||||
this.volume = percent / 100; // (Math.pow(base, percent / 100) - 1) / (base - 1);
|
||||
|
@ -458,11 +451,22 @@ class AudioStream {
|
|||
buffer.getChannelData(0).set(left);
|
||||
buffer.getChannelData(1).set(right);
|
||||
}
|
||||
// setRealSampleRate(sampleRate: number) {
|
||||
// if (sampleRate == this.sampleFormat.rate) {
|
||||
// this.correctAfterXFrames = 0;
|
||||
// }
|
||||
// else {
|
||||
// this.correctAfterXFrames = Math.ceil((this.sampleFormat.rate / sampleRate) / (this.sampleFormat.rate / sampleRate - 1.));
|
||||
// console.debug("setRealSampleRate: " + sampleRate + ", correct after X: " + this.correctAfterXFrames);
|
||||
// }
|
||||
// }
|
||||
chunk = undefined;
|
||||
volume = 1;
|
||||
muted = false;
|
||||
lastLog = 0;
|
||||
}
|
||||
class TimeProvider {
|
||||
constructor(ctx = undefined) {
|
||||
this.diffBuffer = new Array();
|
||||
this.diff = 0;
|
||||
if (ctx) {
|
||||
this.setAudioContext(ctx);
|
||||
}
|
||||
|
@ -508,13 +512,14 @@ class TimeProvider {
|
|||
serverTime(localTimeMs) {
|
||||
return localTimeMs + this.diff;
|
||||
}
|
||||
diffBuffer = new Array();
|
||||
diff = 0;
|
||||
ctx;
|
||||
}
|
||||
class SampleFormat {
|
||||
constructor() {
|
||||
this.rate = 48000;
|
||||
this.channels = 2;
|
||||
this.bits = 16;
|
||||
}
|
||||
rate = 48000;
|
||||
channels = 2;
|
||||
bits = 16;
|
||||
msRate() {
|
||||
return this.rate / 1000;
|
||||
}
|
||||
|
@ -568,8 +573,6 @@ class OpusDecoder extends Decoder {
|
|||
class FlacDecoder extends Decoder {
|
||||
constructor() {
|
||||
super();
|
||||
this.header = null;
|
||||
this.cacheInfo = { isCachedChunk: false, cachedBlocks: 0 };
|
||||
this.decoder = Flac.create_libflac_decoder(true);
|
||||
if (this.decoder) {
|
||||
let init_status = Flac.init_decoder_stream(this.decoder, this.read_callback_fn.bind(this), this.write_callback_fn.bind(this), this.error_callback_fn.bind(this), this.metadata_callback_fn.bind(this), false);
|
||||
|
@ -657,10 +660,15 @@ class FlacDecoder extends Decoder {
|
|||
Flac.FLAC__stream_decoder_process_until_end_of_metadata(this.decoder);
|
||||
return this.sampleFormat;
|
||||
}
|
||||
sampleFormat;
|
||||
decoder;
|
||||
header = null;
|
||||
flacChunk;
|
||||
pcmChunk;
|
||||
cacheInfo = { isCachedChunk: false, cachedBlocks: 0 };
|
||||
}
|
||||
class PlayBuffer {
|
||||
constructor(buffer, playTime, source, destination) {
|
||||
this.num = 0;
|
||||
this.buffer = buffer;
|
||||
this.playTime = playTime;
|
||||
this.source = source;
|
||||
|
@ -668,12 +676,17 @@ class PlayBuffer {
|
|||
this.source.connect(destination);
|
||||
this.onended = (_playBuffer) => { };
|
||||
}
|
||||
onended;
|
||||
start() {
|
||||
this.source.onended = () => {
|
||||
this.onended(this);
|
||||
};
|
||||
this.source.start(this.playTime);
|
||||
}
|
||||
buffer;
|
||||
playTime;
|
||||
source;
|
||||
num = 0;
|
||||
}
|
||||
class PcmDecoder extends Decoder {
|
||||
setHeader(buffer) {
|
||||
|
@ -690,19 +703,6 @@ class PcmDecoder extends Decoder {
|
|||
}
|
||||
class SnapStream {
|
||||
constructor(baseUrl) {
|
||||
this.playTime = 0;
|
||||
this.msgId = 0;
|
||||
this.bufferDurationMs = 80; // 0;
|
||||
this.bufferFrameCount = 3844; // 9600; // 2400;//8192;
|
||||
this.syncHandle = -1;
|
||||
// ageBuffer: Array<number>;
|
||||
this.audioBuffers = new Array();
|
||||
this.freeBuffers = new Array();
|
||||
// median: number = 0;
|
||||
this.audioBufferCount = 3;
|
||||
this.bufferMs = 1000;
|
||||
this.bufferNum = 0;
|
||||
this.latency = 0;
|
||||
this.baseUrl = baseUrl;
|
||||
this.timeProvider = new TimeProvider();
|
||||
if (this.setupAudioContext()) {
|
||||
|
@ -734,6 +734,9 @@ class SnapStream {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
static getClientId() {
|
||||
return getPersistentValue("uniqueId", uuidv4());
|
||||
}
|
||||
connect() {
|
||||
this.streamsocket = new WebSocket(this.baseUrl + '/stream');
|
||||
this.streamsocket.binaryType = "arraybuffer";
|
||||
|
@ -745,7 +748,9 @@ class SnapStream {
|
|||
hello.arch = "web";
|
||||
hello.os = navigator.platform;
|
||||
hello.hostname = "Snapweb client";
|
||||
hello.uniqueId = getPersistentValue("uniqueId", uuidv4());
|
||||
hello.uniqueId = SnapStream.getClientId();
|
||||
const versionElem = document.getElementsByTagName("meta").namedItem("version");
|
||||
hello.version = versionElem ? versionElem.content : "0.0.0";
|
||||
this.sendMessage(hello);
|
||||
this.syncTime();
|
||||
this.syncHandle = window.setInterval(() => this.syncTime(), 1000);
|
||||
|
@ -888,5 +893,27 @@ class SnapStream {
|
|||
playBuffer.start();
|
||||
this.playTime += this.bufferFrameCount / this.sampleFormat.rate;
|
||||
}
|
||||
baseUrl;
|
||||
streamsocket;
|
||||
playTime = 0;
|
||||
msgId = 0;
|
||||
bufferDurationMs = 80; // 0;
|
||||
bufferFrameCount = 3844; // 9600; // 2400;//8192;
|
||||
syncHandle = -1;
|
||||
// ageBuffer: Array<number>;
|
||||
audioBuffers = new Array();
|
||||
freeBuffers = new Array();
|
||||
timeProvider;
|
||||
stream;
|
||||
ctx; // | undefined;
|
||||
gainNode;
|
||||
serverSettings;
|
||||
decoder;
|
||||
sampleFormat;
|
||||
// median: number = 0;
|
||||
audioBufferCount = 3;
|
||||
bufferMs = 1000;
|
||||
bufferNum = 0;
|
||||
latency = 0;
|
||||
}
|
||||
//# sourceMappingURL=snapstream.js.map
|
|
@ -28,6 +28,10 @@ body {
|
|||
background: #555;
|
||||
}
|
||||
|
||||
input, textarea, button, select, a {
|
||||
-webkit-tap-highlight-color: rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
.navbar {
|
||||
overflow: hidden;
|
||||
background-color: #607d8b;
|
||||
|
@ -129,6 +133,7 @@ select {
|
|||
grid-template-columns: min-content auto min-content;
|
||||
grid-template-rows: min-content min-content;
|
||||
grid-gap: 0px;
|
||||
/* align-items: center;*/
|
||||
}
|
||||
|
||||
/* .client:hover {
|
||||
|
@ -185,13 +190,24 @@ select {
|
|||
color: #686868;
|
||||
grid-row: 2;
|
||||
grid-column: 1;
|
||||
/* top: 50%;*/
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/*
|
||||
.cover-img {
|
||||
color: #686868;
|
||||
grid-row: 2;
|
||||
grid-column: 1;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
padding: 5px;
|
||||
text-decoration: none;
|
||||
}
|
||||
*/
|
||||
.sliderdiv {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue