vybe/client/stream.js

163 lines
3.9 KiB
JavaScript

import { render, html } from '/uhtml.js';
let streamid;
let handle;
let mediaStream;
let recorder;
async function stream() {
if (handle) {
clearInterval(handle);
handle = null;
if (recorder.state === 'recording')
recorder.stop();
window.emit('stream', {
id: streamid,
thread: window.currentThread.id,
stop: true
});
document.getElementById('streaming').innerText = 'start streaming';
return;
}
if (!mediaStream)
mediaStream = await navigator.mediaDevices.getUserMedia({
audio: {
autoGainControl: false,
echoCancellation: false,
noiseSuppression: false,
sampleRate: 48000,
sampleSize: 16
}
});
if (!mediaStream)
return;
window.emit('stream', {
thread: window.currentThread.id,
name: document.getElementById('streamname').value
}, msg => {
if (!msg.success) {
console.log('stream failed: ', msg.message);
return;
}
streamid = msg.id;
document.getElementById('streaming').innerText = 'stop streaming';
});
function record() {
let r = recorder = new MediaRecorder(mediaStream);
let chunks = [];
r.ondataavailable = event => {
if (!event.data.size)
return;
chunks.push(event.data);
};
r.onstop = async () => {
if (!chunks.length || !handle)
return;
//console.log(`${Date.now()} ${chunks.length}`);
window.emit('streamdata', {
id: streamid,
audio: await (new Blob(chunks, { type: chunks[0].type })).arrayBuffer()
}, msg => {
if (!msg.success)
console.log('streamdata failed: ', msg.message);
});
};
r.onstart = () => {
setTimeout(() => r.state === 'recording' && r.stop(), 100);
};
r.start();
}
record();
handle = setInterval(record, 100);
}
let audioctx;
let streaming = {};
function addStream(stream) {
let p = html.node`
<p>
<button id='play' onclick=${e => {
if (stream.playing) {
audioctx.suspend();
delete streaming[stream.id];
stream.playing = false;
e.target.innerText = '▶';
}
else {
audioctx = new AudioContext();
streaming[stream.id] = stream;
stream.playing = true;
e.target.innerText = '⏹';
}
window.emit('play_stream', {
id: stream.id,
thread: window.currentThread.id,
playing: stream.playing
}, msg => {
if (!msg.success)
console.log('play stream failed: ', msg.message);
});
}}>▶</button>
${stream.user}<span id='name'>${stream.name ? ` - ${stream.name}` : ''}</span>
</p>`;
p.id = 'stream' + stream.id;
document.getElementById('streams').append(p);
}
function loadStreams() {
let div = document.getElementById('stream');
div.innerHTML = '';
if (window.currentThread.permissions.post) {
// why doesn't html` work here? html.node` does
render(div, html.node`
<button id='streaming' onclick=${stream}>start streaming</button>
<span>stream name:</span>
<input id='streamname'>`);
div.children['streamname'].oninput = event => {
if (handle)
window.emit('stream', {
id: streamid,
thread: window.currentThread.id,
name: div.children['streamname'].value
});
};
}
div.insertAdjacentHTML('beforeend', `
<p>streams:</p>
<div id='streams'></div>`);
for (let stream of window.currentThread.streams)
addStream(stream);
}
window.socket.on('stream', async msg => {
if (msg.thread !== window.currentThread?.id)
return;
let p = document.getElementById('stream' + msg.id);
if (p) {
if (msg.stopped) {
p.remove();
window.currentThread.streams.splice(
window.currentThread.streams.findIndex(s => s.id === msg.id), 1);
}
else
p.children['name'].innerText = msg.name ? ' - ' + msg.name : '';
}
else if (!msg.stopped) {
window.currentThread.streams.push(msg);
addStream(msg);
}
});
window.socket.on('streamdata', async msg => {
if (!streaming[msg.id])
return;
let source = audioctx.createBufferSource();
source.buffer = await audioctx.decodeAudioData(msg.audio);
source.connect(audioctx.destination);
source.start(/*audioStartTime*/);
});
export default loadStreams;