streams, message time, space zoom+pan
parent
bb31b56929
commit
779aaece08
|
@ -162,6 +162,7 @@
|
|||
}
|
||||
> .title {
|
||||
width: 100%;
|
||||
padding-inline: 4px;
|
||||
}
|
||||
}
|
||||
.expander {
|
||||
|
@ -301,6 +302,12 @@
|
|||
.message {
|
||||
margin-bottom: 5px;
|
||||
overflow-wrap: anywhere;
|
||||
display: flex;
|
||||
}
|
||||
.time {
|
||||
color: #999;
|
||||
font-size: small;
|
||||
vertical-align: top;
|
||||
}
|
||||
#loadmore {
|
||||
margin-bottom: 10px;
|
||||
|
@ -321,9 +328,12 @@
|
|||
}
|
||||
#space {
|
||||
margin: -2px; /* offset column margin */
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
}
|
||||
#spacediv {
|
||||
position: relative;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
.span {
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
|
|
|
@ -1,18 +1,6 @@
|
|||
import { render, html } from '/uhtml.js';
|
||||
|
||||
let msg;
|
||||
|
||||
function sendMessage(event) {
|
||||
event.preventDefault();
|
||||
if (!msg.value)
|
||||
return;
|
||||
window.currentInstance.emit('send_message', {
|
||||
message: msg.value,
|
||||
thread: window.currentThread.id
|
||||
});
|
||||
msg.value = '';
|
||||
}
|
||||
|
||||
let earliestMessage;
|
||||
|
||||
function loadMessages(callback) {
|
||||
|
@ -24,7 +12,16 @@ function loadMessages(callback) {
|
|||
load more messages
|
||||
</button>
|
||||
<div id='messages'></div>
|
||||
<form id='msginput' onsubmit=${sendMessage}>
|
||||
<form id='msginput' onsubmit=${function(event) {
|
||||
event.preventDefault();
|
||||
if (!msg.value)
|
||||
return;
|
||||
window.currentInstance.emit('send_message', {
|
||||
message: msg.value,
|
||||
thread: window.currentThread.id
|
||||
});
|
||||
msg.value = '';
|
||||
}}>
|
||||
<input id='msg' placeholder='write a message...' />
|
||||
<button type='submit' id='sendmsg'>send</button>
|
||||
</form>
|
||||
|
@ -40,6 +37,20 @@ function loadMessages(callback) {
|
|||
else
|
||||
document.getElementById('msginput').classList.add('hidden');
|
||||
}
|
||||
|
||||
function addMessage(message, user) {
|
||||
let now = new Date(), date = new Date(message.created);
|
||||
let div = html.node`<div class='message'><span class='time'>${
|
||||
now.getDate() === date.getDate() && date.getTime() > now.getTime() - 24 * 60 * 60000
|
||||
? date.toLocaleTimeString() : date.toLocaleString()
|
||||
}</span></div>`;
|
||||
let content = html.node`<span class='content'>: ${message.content}</span>`;
|
||||
content.prepend(user ? window.makeUser(user, user.id.split?.('@')[1] || instance.url)
|
||||
: html.node`<span>${message.user.id}</span>`);
|
||||
div.prepend(content);
|
||||
messages.append(div);
|
||||
}
|
||||
|
||||
instance.emit('get_history', {
|
||||
before: earliestMessage,
|
||||
thread: window.currentThread.id
|
||||
|
@ -73,10 +84,7 @@ function loadMessages(callback) {
|
|||
user.permissions = message.user.permissions;
|
||||
}
|
||||
}
|
||||
let div = html.node`<div class='message'>: ${message.content}</div>`;
|
||||
div.prepend(user ? window.makeUser(user, user.id.split?.('@')[1] || instance.url)
|
||||
: html.node`<span>${message.user.id}</span>`);
|
||||
messages.prepend(div);
|
||||
addMessage(message, user);
|
||||
}
|
||||
}
|
||||
if (msg.more)
|
||||
|
@ -92,10 +100,7 @@ function loadMessages(callback) {
|
|||
return;
|
||||
const messages = document.getElementById('messages');
|
||||
let scroll = messages.scrollTop + 10 >= messages.scrollHeight - messages.clientHeight;
|
||||
let div = html.node`<div class='message'>: ${message.content}</div>`;
|
||||
div.prepend(window.makeUser(message.user,
|
||||
message.user.id.split?.('@')[1] || instance.url));
|
||||
messages.append(div);
|
||||
addMessage(message, message.user);
|
||||
if (scroll)
|
||||
messages.scroll(0, messages.scrollHeight - messages.clientHeight);
|
||||
if (!earliestMessage)
|
||||
|
|
|
@ -1,23 +1,30 @@
|
|||
let space;
|
||||
|
||||
let scale = 1; // todo: make zooming work
|
||||
let spaceContainer, space;
|
||||
|
||||
let editing;
|
||||
let dragging;
|
||||
let moved;
|
||||
let movedFrom;
|
||||
let offset;
|
||||
let clicked;
|
||||
|
||||
document.onmousemove = event => {
|
||||
document.addEventListener('mouseup', function(event) {
|
||||
clicked = false;
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', function(event) {
|
||||
moved = true;
|
||||
if (!dragging)
|
||||
return;
|
||||
let left = (event.clientX - space.offsetLeft) * scale - offset.x;
|
||||
let top = (event.clientY - space.offsetTop) * scale - offset.y;
|
||||
if (dragging) {
|
||||
let left = event.clientX - spaceContainer.offsetLeft - offset.x;
|
||||
let top = event.clientY - spaceContainer.offsetTop - offset.y;
|
||||
dragging.style.left = `${left < 0 ? 0 : left}px`;
|
||||
dragging.style.top = `${top < 0 ? 0 : top}px`;
|
||||
save(dragging);
|
||||
};
|
||||
}
|
||||
else if (clicked) {
|
||||
spaceContainer.scrollLeft -= event.movementX;
|
||||
spaceContainer.scrollTop -= event.movementY;
|
||||
}
|
||||
});
|
||||
|
||||
let saving;
|
||||
let queue;
|
||||
|
@ -79,24 +86,28 @@ function add(s) {
|
|||
};
|
||||
span.onwheel = function(event) {
|
||||
event.preventDefault();
|
||||
if (event.deltaY < 0 && this.scale >= 200)
|
||||
return;
|
||||
event.stopPropagation();
|
||||
this.scale *= 1 - event.deltaY * .001;
|
||||
if (this.scale > 100)
|
||||
this.scale = 100;
|
||||
this.style.transform = `translate(-50%, -50%) scale(${this.scale})`;
|
||||
save(this);
|
||||
};
|
||||
span.onmousedown = function(event) {
|
||||
if (dragging || editing === this)
|
||||
return;
|
||||
dragging = this;
|
||||
event.preventDefault();
|
||||
if (event.button !== 0)
|
||||
return;
|
||||
dragging = this;
|
||||
offset = {
|
||||
x: event.clientX - (space.offsetLeft + this.offsetLeft),
|
||||
y: event.clientY - (space.offsetTop + this.offsetTop)
|
||||
x: event.clientX - (spaceContainer.offsetLeft + this.offsetLeft),
|
||||
y: event.clientY - (spaceContainer.offsetTop + this.offsetTop)
|
||||
};
|
||||
};
|
||||
span.onmouseup = function(event) {
|
||||
event.stopPropagation();
|
||||
if (event.button !== 0)
|
||||
return;
|
||||
dragging = null;
|
||||
if (moved)
|
||||
return;
|
||||
|
@ -110,13 +121,35 @@ function add(s) {
|
|||
export default function loadSpace(callback) {
|
||||
let instance = window.currentInstance;
|
||||
|
||||
if (!instance.spaceid) {
|
||||
space = document.getElementById('space');
|
||||
space.onmousedown = event => {
|
||||
let scale = 1;
|
||||
|
||||
if (!instance.spaced) {
|
||||
spaceContainer = document.getElementById('space');
|
||||
space = document.getElementById('spacediv');
|
||||
|
||||
spaceContainer.onwheel = function(event) {
|
||||
if (event.getModifierState('Shift'))
|
||||
return;
|
||||
event.preventDefault();
|
||||
scale *= 1 - event.deltaY * .001;
|
||||
if (scale < .5)
|
||||
scale = .5;
|
||||
space.style.transform = `scale(${scale})`;
|
||||
spaceContainer.scrollLeft += event.offsetX * (scale - scale / (1 - event.deltaY * .001));
|
||||
spaceContainer.scrollTop += event.offsetY * (scale - scale / (1 - event.deltaY * .001));
|
||||
//spaceContainer.scrollLeft += spaceContainer.clientWidth * (1 - spaceContainer.clientWidth / spaceContainer.scrollWidth + event.offsetX / spaceContainer.clientWidth) * (scale - scale / (1 - event.deltaY * .001));
|
||||
//spaceContainer.scrollTop += spaceContainer.clientHeight * (1 - spaceContainer.clientHeight / spaceContainer.scrollHeight + event.offsetY / spaceContainer.clientHeight) * (scale - scale / (1 - event.deltaY * .001));
|
||||
};
|
||||
spaceContainer.onmousedown = function(event) {
|
||||
if (event.button !== 0)
|
||||
return;
|
||||
clicked = true;
|
||||
moved = false;
|
||||
movedFrom = { x: event.offsetX, y: event.offsetY };
|
||||
};
|
||||
space.onmouseup = event => {
|
||||
spaceContainer.onmouseup = function(event) {
|
||||
if (event.button !== 0)
|
||||
return;
|
||||
if (dragging) {
|
||||
dragging.onmouseup(event);
|
||||
return;
|
||||
|
@ -130,8 +163,8 @@ export default function loadSpace(callback) {
|
|||
+ (event.offsetY - movedFrom.y) * (event.offsetY - movedFrom.y) > 100)
|
||||
return;
|
||||
editing = add({
|
||||
x: event.offsetX + space.scrollLeft,
|
||||
y: event.offsetY + space.scrollTop,
|
||||
x: (event.offsetX + spaceContainer.scrollLeft) / scale,
|
||||
y: (event.offsetY + spaceContainer.scrollTop) / scale,
|
||||
scale: 1,
|
||||
content: ''
|
||||
});
|
||||
|
@ -139,7 +172,7 @@ export default function loadSpace(callback) {
|
|||
};
|
||||
|
||||
instance.socket.on('span', msg => {
|
||||
if (msg.thread !== instance.spaceid || window.currentInstance !== instance)
|
||||
if (msg.thread !== window.currentSpace.id || window.currentInstance !== instance)
|
||||
return;
|
||||
let span = document.getElementById('span' + msg.id);
|
||||
if (span) {
|
||||
|
@ -152,13 +185,15 @@ export default function loadSpace(callback) {
|
|||
else
|
||||
add(msg);
|
||||
});
|
||||
instance.spaced = true;
|
||||
}
|
||||
else if (instance.spaceid === window.currentThread.id)
|
||||
else if (window.currentSpace === window.currentThread)
|
||||
return;
|
||||
instance.spaceid = window.currentThread.id;
|
||||
window.currentSpace = window.currentThread;
|
||||
space.innerHTML = '';
|
||||
space.style.transform = '';
|
||||
instance.emit('get_space', {
|
||||
thread: window.currentThread.id
|
||||
thread: window.currentSpace.id
|
||||
}, msg => {
|
||||
if (!msg.success) {
|
||||
console.log('get space failed: ' + msg.message);
|
||||
|
|
101
client/stream.js
101
client/stream.js
|
@ -2,8 +2,7 @@ import { render, html } from '/uhtml.js';
|
|||
|
||||
let mediaStream;
|
||||
|
||||
async function stream() {
|
||||
let thread = window.currentThread;
|
||||
async function stream(thread) {
|
||||
if (thread.handle) {
|
||||
clearInterval(thread.handle);
|
||||
delete thread.handle;
|
||||
|
@ -35,40 +34,40 @@ async function stream() {
|
|||
thread.instance.emit('stream', {
|
||||
thread: thread.id,
|
||||
name: document.getElementById('streamname').value
|
||||
}, msg => {
|
||||
}, async msg => {
|
||||
if (!msg.success) {
|
||||
console.log('stream failed:', msg.message);
|
||||
return;
|
||||
}
|
||||
thread.streamid = msg.id;
|
||||
document.getElementById('streaming').innerText = 'stop streaming';
|
||||
function record() {
|
||||
let r = thread.recorder = new MediaRecorder(mediaStream);
|
||||
let chunks = [];
|
||||
r.ondataavailable = event => {
|
||||
if (!event.data.size)
|
||||
thread.recorder = new MediaRecorder(mediaStream, {
|
||||
mimeType: 'audio/webm;codecs=opus'
|
||||
});
|
||||
thread.recorder.start();
|
||||
thread.recorder.ondataavailable = async event => {
|
||||
if (!event.data.size || !thread.handle)
|
||||
return;
|
||||
chunks.push(event.data);
|
||||
};
|
||||
r.onstop = async () => {
|
||||
if (!chunks.length || !thread.handle)
|
||||
return;
|
||||
//console.log(`${Date.now()} ${chunks.length}`);
|
||||
thread.instance.emit('streamdata', {
|
||||
id: thread.streamid,
|
||||
audio: await (new Blob(chunks, { type: chunks[0].type })).arrayBuffer()
|
||||
audio: await event.data.arrayBuffer()
|
||||
}, msg => {
|
||||
if (!msg.success)
|
||||
if (msg.success)
|
||||
return;
|
||||
console.log('streamdata failed:', msg.message);
|
||||
if (msg.message === 'stream not found' && thread.handle)
|
||||
stream(thread);
|
||||
});
|
||||
};
|
||||
r.onstart = () => {
|
||||
setTimeout(() => r.state === 'recording' && r.stop(), 500);
|
||||
};
|
||||
r.start();
|
||||
}
|
||||
record();
|
||||
thread.handle = setInterval(record, 500);
|
||||
// first 200ms chunk will be used as stream header
|
||||
thread.handle = setTimeout(() => {
|
||||
thread.recorder.requestData();
|
||||
thread.handle = setInterval(() => {
|
||||
if (!thread.handle)
|
||||
return;
|
||||
thread.recorder.requestData();
|
||||
}, 500);
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -79,24 +78,24 @@ function loadStreams() {
|
|||
div.innerHTML = '';
|
||||
if (window.currentThread?.permissions.post) {
|
||||
render(div, html.node`
|
||||
<button id='streaming' onclick=${stream}>
|
||||
${window.currentThread.handle ? 'stop' : 'start'} streaming
|
||||
</button>
|
||||
<button id='streaming' onclick=${function(event){
|
||||
stream(currentThread);
|
||||
}}>${currentThread.handle ? 'stop' : 'start'} streaming</button>
|
||||
<span>stream name:</span>
|
||||
<input id='streamname' oninput=${function(event) {
|
||||
if (window.currentThread.handle)
|
||||
if (currentThread.handle)
|
||||
instance.emit('stream', {
|
||||
id: window.currentThread.streamid,
|
||||
thread: window.currentThread.id,
|
||||
id: currentThread.streamid,
|
||||
thread: currentThread.id,
|
||||
name: this.value
|
||||
});
|
||||
window.currentThread.streamname = this.value;
|
||||
currentThread.streamname = this.value;
|
||||
}}>
|
||||
<p id='listeners'>${window.currentThread.listeners ?
|
||||
window.currentThread.listeners + ' listeners' : ''}
|
||||
<p id='listeners'>${currentThread.listeners ?
|
||||
currentThread.listeners + ' listeners' : ''}
|
||||
</p>`);
|
||||
if (window.currentThread.streamname)
|
||||
document.getElementById('streamname').value = window.currentThread.streamname;
|
||||
if (currentThread.streamname)
|
||||
document.getElementById('streamname').value = currentThread.streamname;
|
||||
}
|
||||
div.insertAdjacentHTML('beforeend', `
|
||||
<p>streams:</p>
|
||||
|
@ -107,16 +106,23 @@ function loadStreams() {
|
|||
<p>
|
||||
<button id='play' onclick=${function(event) {
|
||||
if (stream.playing) {
|
||||
stream.audioctx.suspend();
|
||||
stream.audio.pause();
|
||||
delete instance.streaming[stream.id];
|
||||
stream.playing = false;
|
||||
this.innerText = '▶';
|
||||
}
|
||||
else {
|
||||
stream.audioctx = new AudioContext();
|
||||
stream.gain = stream.audioctx.createGain();
|
||||
stream.gain.connect(stream.audioctx.destination);
|
||||
stream.gain.gain.value = p.children['volume'].value / 100;
|
||||
stream.audio = new Audio();
|
||||
// play 200ms stream header silently
|
||||
stream.audio.volume = 0;
|
||||
stream.audio.onplaying = () => setTimeout(() => {
|
||||
stream.audio.volume = p.children['volume'].value / 100;
|
||||
p.children['volume'].oninput = function(event) {
|
||||
stream.audio.volume = this.value / 100;
|
||||
};
|
||||
}, 200);
|
||||
stream.audio.src = '/stream/' + stream.id;
|
||||
stream.audio.play();
|
||||
instance.streaming[stream.id] = stream;
|
||||
stream.playing = true;
|
||||
this.innerText = '⏹';
|
||||
|
@ -127,13 +133,10 @@ function loadStreams() {
|
|||
playing: stream.playing
|
||||
}, msg => {
|
||||
if (!msg.success)
|
||||
console.log('play stream failed: ', msg.message);
|
||||
console.log('play_stream failed: ', msg.message);
|
||||
});
|
||||
}}>${instance.streaming[stream.id] ? '⏹' : '▶'}</button>
|
||||
<input id='volume' type='range' oninput=${function(event) {
|
||||
if (stream.gain)
|
||||
stream.gain.gain.value = this.value / 100;
|
||||
}}>
|
||||
<input id='volume' type='range'>
|
||||
<span id='name'>${stream.name ? ` - ${stream.name}` : ''}</span>
|
||||
</p>`;
|
||||
p.insertBefore(window.makeUser(stream.user,
|
||||
|
@ -171,16 +174,6 @@ function loadStreams() {
|
|||
}
|
||||
});
|
||||
|
||||
instance.socket.on('streamdata', async msg => {
|
||||
let stream = instance.streaming[msg.id];
|
||||
if (!stream)
|
||||
return;
|
||||
let source = stream.audioctx.createBufferSource();
|
||||
source.buffer = await stream.audioctx.decodeAudioData(msg.audio);
|
||||
source.connect(stream.gain);
|
||||
source.start(/*audioStartTime*/);
|
||||
});
|
||||
|
||||
instance.socket.on('listeners', msg => {
|
||||
document.querySelector(
|
||||
`#instance${instance.id} > #threads > #threadlist > #thread${msg.thread}`)
|
||||
|
|
|
@ -419,7 +419,9 @@ async function loadThreads(instancediv, select) {
|
|||
}>members</button>
|
||||
</div>
|
||||
<div id='message' class='tabcontent'></div>
|
||||
<div id='space' class='tabcontent hidden'></div>
|
||||
<div id='space' class='tabcontent hidden'>
|
||||
<div id='spacediv'></div>
|
||||
</div>
|
||||
<div id='call' class='tabcontent hidden'></div>
|
||||
</div>
|
||||
<hr class='separator' color='#505050'>
|
||||
|
|
15
server.js
15
server.js
|
@ -4,6 +4,7 @@ const http = require('http');
|
|||
const { Server } = require('socket.io');
|
||||
const ioclient = require('socket.io-client');
|
||||
const compression = require('compression');
|
||||
const { PassThrough } = require('stream');
|
||||
|
||||
const events = {};
|
||||
for (let file of fs.readdirSync('./src/events')) {
|
||||
|
@ -89,6 +90,20 @@ const io = new Server(server, {
|
|||
|
||||
app.use(express.static('client'));
|
||||
|
||||
app.get('/stream/:id', (req, res) => {
|
||||
let stream = vybe.streams[req.params.id];
|
||||
if (!stream) {
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
res.status(200);
|
||||
let passthrough = new PassThrough();
|
||||
passthrough.pipe(res);
|
||||
stream.streams.add(passthrough);
|
||||
res.set('Content-Type', 'audio/webm');
|
||||
req.on('close', () => stream.streams.delete(passthrough));
|
||||
});
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
for (let event in events) {
|
||||
socket.on(event, (msg, callback) => {
|
||||
|
|
|
@ -46,7 +46,8 @@ async function send_message(msg, respond) {
|
|||
permissions: userperms
|
||||
},
|
||||
content: msg.message,
|
||||
thread: msg.thread
|
||||
thread: msg.thread,
|
||||
created: Date.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -77,12 +78,12 @@ async function get_history(msg, respond) {
|
|||
});
|
||||
}
|
||||
const messages = (await db.query(`
|
||||
select coalesce(displayname, name) as displayname, name, user as userid, post.id, content
|
||||
select coalesce(displayname, name) as displayname, name, user as userid, post.id, content, post.created
|
||||
from post
|
||||
left join user on post.user = user.id
|
||||
where ${msg.before ? 'post.id < ? and' : ''}
|
||||
thread = ?
|
||||
order by post.created desc
|
||||
order by post.created asc
|
||||
limit 101`,
|
||||
msg.before ? [msg.before, msg.thread] : [msg.thread]
|
||||
)).rows;
|
||||
|
@ -103,7 +104,8 @@ async function get_history(msg, respond) {
|
|||
displayname: message.displayname,
|
||||
permissions: perms[message.userid]
|
||||
},
|
||||
content: message.content
|
||||
content: message.content,
|
||||
created: new Date(message.created + 'Z').getTime()
|
||||
})),
|
||||
more: messages.length > 100
|
||||
});
|
||||
|
|
|
@ -32,24 +32,22 @@ async function stream(msg, respond, socket) {
|
|||
}
|
||||
}
|
||||
if (typeof msg.id === 'number') {
|
||||
stream = vybe.streams[msg.id];
|
||||
if (!stream)
|
||||
let vstream = vybe.streams[msg.id];
|
||||
if (!vstream)
|
||||
return respond({
|
||||
success: false,
|
||||
message: 'stream not found'
|
||||
});
|
||||
if (msg.auth_user.id !== stream.userid)
|
||||
if (msg.auth_user.id !== vstream.userid)
|
||||
return respond({
|
||||
success: false,
|
||||
message: "stream doesn't belong to user"
|
||||
});
|
||||
stream = vstream.stream;
|
||||
if (msg.stop) {
|
||||
await stream.stop();
|
||||
return respond({
|
||||
success: true
|
||||
});
|
||||
await vstream.stop();
|
||||
return respond({ success: true });
|
||||
}
|
||||
stream = stream.stream;
|
||||
stream.name = msg.name;
|
||||
stream.user.displayname = msg.auth_user.displayname;
|
||||
}
|
||||
|
@ -82,7 +80,8 @@ async function stream(msg, respond, socket) {
|
|||
thread.streams.splice(thread.streams.findIndex(s => s.id === stream.id), 1);
|
||||
delete vybe.streams[stream.id];
|
||||
await send();
|
||||
}
|
||||
},
|
||||
streams: new Set()
|
||||
};
|
||||
}
|
||||
await send();
|
||||
|
@ -106,15 +105,11 @@ async function streamdata(msg, respond) {
|
|||
message: "stream doesn't belong to user"
|
||||
});
|
||||
}
|
||||
for (let id in stream.listeners)
|
||||
stream.listeners[id].emit('streamdata', {
|
||||
id: msg.id,
|
||||
audio: msg.audio,
|
||||
video: msg.video
|
||||
});
|
||||
respond({
|
||||
success: true
|
||||
});
|
||||
if (stream.head)
|
||||
stream.streams.forEach(passthrough => passthrough.write(msg.audio));
|
||||
else
|
||||
stream.head = msg.audio;
|
||||
respond({ success: true });
|
||||
}
|
||||
|
||||
async function play_stream(msg, respond, socket) {
|
||||
|
@ -138,9 +133,7 @@ async function play_stream(msg, respond, socket) {
|
|||
thread: stream.stream.thread,
|
||||
count: Object.keys(stream.listeners).length
|
||||
});
|
||||
respond({
|
||||
success: true
|
||||
});
|
||||
respond({ success: true });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
|
Loading…
Reference in New Issue