Однажды я смотрел один фильм и отпечатывается в памяти момент когда один из главных героев фильма тактично поднимает ногу и руку в ритм мелодии.
Так вот: примечателен не столько герой, сколько множество мониторов, стоящих за его спиной.
В последнее время у меня, как и у многих других, сильно увеличилось количество времени, проводимого в четырех стенах, и мне пришла в голову идея: «Какой самый странный способ реализовать такую сценуЭ» Забегая вперед скажу, что выбор пал на использование веб-браузера, а именно WebRTC и WebAudio API. Вот я и подумал, и тут сразу начались варианты, от простых (поднять магнитолу и найти плеер, у которого есть такая визуализация), до длинных (сделать клиент-серверное приложение, которое будет отправлять информацию о цвете через сокет).
И тут я поймал себя на мысли: «теперь в браузере есть все необходимые для реализации компоненты», вот и попробую сделать это с его помощью.
WebRTC — это способ передачи данных из браузера в браузер (peer-to-peer), а значит, мне не придется делать сервер, подумал я сначала.
Но я немного ошибся.
Для того, чтобы сделать RTCPeerConnection, нужны два сервера: сигнальный и ICE. Для второго можно использовать готовое решение (серверы STUN или TURN есть в репозиториях многих дистрибутивов Linux).
С первым нужно что-то делать.
В документации сказано, что в качестве сигнала может выступать произвольный протокол двусторонней связи, и здесь можно сразу использовать WebSockets, Longpooling или сделать что-то свое.
Мне кажется, самый простой вариант — взять hello world из документации какой-нибудь библиотеки.
А вот этот простой сервер сигнализации:
Я даже не стал реализовывать обработку клиентских WebSockets-сообщений, а просто сделал POST-эндпойнт, который отправляет сообщение всем желающим.import os from aiohttp import web, WSMsgType routes = web.RouteTableDef() routes.static("/def", os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static') ) @routes.post('/broadcast') async def word(request): for conn in list(ws_client_connections): data = await request.text() await conn.send_str(data) return web.Response(text="Hello, world") ws_client_connections = set() async def websocket_handler(request): ws = web.WebSocketResponse(autoping=True) await ws.prepare(request) ws_client_connections.add(ws) async for msg in ws: if msg.type == WSMsgType.TEXT: if msg.data == 'close': await ws.close() # del ws_client_connections[user_id] else: continue elif msg.type == WSMsgType.ERROR: print('conn lost') # del ws_client_connections[user_id] return ws if __name__ == '__main__': app = web.Application() app.add_routes(routes) app.router.add_get('/ws', websocket_handler) web.run_app(app)
Мне нравится подход копирования документации.
Далее, чтобы установить WebRTC-соединение между браузерами, происходит простое «привет-привет», и вы можете — и я могу.
На схеме это очень наглядно показано:
(Схема взята из страницы ) Для начала нужно создать само соединение: function openConnection() {
const servers = { iceServers: [
{
urls: [`turn:${window.location.hostname}`],
username: 'rtc',
credential: 'demo'
},
{
urls: [`stun:${window.location.hostname}`]
}
]};
let localConnection = new RTCPeerConnection(servers);
console.log('Created local peer connection object localConnection');
dataChannelSend.placeholder = '';
localConnection.ondatachannel = receiveChannelCallback;
localConnection.ontrack = e => {
consumeRemoteStream(localConnection, e);
}
let sendChannel = localConnection.createDataChannel('sendDataChannel');
console.log('Created send data channel');
sendChannel.onopen = onSendChannelStateChange;
sendChannel.onclose = onSendChannelStateChange;
return localConnection;
}
Здесь важно, что прежде чем устанавливать само соединение, нужно задать цель подключения.
Вызовы createDataChannel() и более поздние addTrack() делают именно это.
function createConnection() {
if(!iAmHost){
alert('became host')
return 0;
}
for (let listener of streamListeners){
streamListenersConnections[listener] = openConnection()
streamListenersConnections[listener].
onicecandidate = e => { onIceCandidate(streamListenersConnections[listener], e, listener); }; audioStreamDestination.stream.getTracks().
forEach(track => { streamListenersConnections[listener].
addTrack(track.clone()) }) // localConnection.getStats().
then(it => console.log(it)) streamListenersConnections[listener].
createOffer().
then((offer) => gotDescription1(offer, listener), onCreateSessionDescriptionError ).
then( () => {
startButton.disabled = true;
closeButton.disabled = false;
});
}
}
На этом этапе пора вспомнить об API WebAudio. Поскольку в браузере есть возможность загружать двоичный файл в память и даже декодировать его, если формат поддерживается, вам необходимо использовать эту возможность.
function createAudioBufferFromFile(){
let fileToProcess = new FileReader();
fileToProcess.readAsArrayBuffer(selectedFile.files[0]);
fileToProcess.onload = () => {
let audioCont = new AudioContext();
audioCont.decodeAudioData(fileToProcess.result).
then(res => {
//TODO: stream to webrtcnode
let source = audioCont.createBufferSource()
audioSource = source;
source.buffer = res;
let dest = audioCont.createMediaStreamDestination()
source.connect(dest)
audioStreamDestination =dest;
source.loop = false;
// source.start(0)
iAmHost = true;
});
}
}
Допустим, в этой функции mp3-файл считывается в память.
После чтения создается AudioContext, затем декодируется и превращается в MediaStream. Вуаля, у нас есть аудиопоток, который можно передать через метод addTrack() соединения WebRTC. Чуть раньше функция createOffer() вызывалась при каждом соединении и отправлялась соответствующему клиенту.
Клиент, в свою очередь, принимает предложение и уведомляет инициатора: function acceptDescription2(desc, broadcaster) {
return localConnection.setRemoteDescription(desc)
.
then( () => { return localConnection.createAnswer(); }) .
then(answ => { return localConnection.setLocalDescription(answ); }).
then(() => {
postData(JSON.stringify({type: "accept", user:username.value, to:broadcaster, descr:localConnection.currentLocalDescription}));
})
}
Далее инициатор обрабатывает ответ и инициирует обмен IceCandiates: function finalizeCandidate(val, listener) {
console.log('accepting connection')
const a = new RTCSessionDescription(val);
streamListenersConnections[listener].
setRemoteDescription(a).
then(() => {
dataChannelSend.disabled = false;
dataChannelSend.focus();
sendButton.disabled = false;
processIceCandiates(listener)
});
}
Выглядит предельно просто: let conn = localConnection? localConnection: streamListenersConnections[data.user]
conn.addIceCandidate(data.candidate).
then( onAddIceCandidateSuccess, onAddIceCandidateError);
processIceCandiates(data.user)
В результате каждое соединение через addIceCandidate() изучает возможные транспорты противоположного конца соединения.
На данный момент уже есть один основной браузер и несколько браузеров, готовых его слушать.
Остаётся только принять аудиопоток и воспроизвести/(отобразить нужную часть спектра в цвете экрана).
function consumeRemoteStream(localConnection, event) {
console.log('consume remote stream')
const styles = {
position: 'absolute',
height: '100%',
width: '100%',
top: '0px',
left: '0px',
'z-index': 1000
};
Object.keys(styles).
map(i => {
canvas.style[i] = styles[i];
})
let audioCont = new AudioContext();
audioContext = audioCont;
let stream = new MediaStream();
stream.addTrack(event.track)
let source = audioCont.createMediaStreamSource(stream)
let analyser = audioCont.createAnalyser();
analyser.smoothingTimeConstant = 0;
analyser.fftSize = 2048;
source.connect(analyser);
audioAnalizer = analyser;
audioSource = source;
analyser.connect(audioCont.destination)
render()
}
Небольшая манипуляция со стилями, чтобы заполнить всю страницу холстом.
Затем мы создаем AudioContext, который поступает через соединение, и добавляем два узла-потребителя AudioNode. Один из них имеет возможность получить спектрограмму, не зря метод называется createAnalyser().
Завершающий штрих: function render(){
var freq = new Uint8Array(audioAnalizer.frequencyBinCount);
audioAnalizer.getByteFrequencyData(freq);
let band = freq.slice(
Math.floor(freqFrom.value/audioContext.sampleRate*2*freq.length),
Math.floor(freqTo.value/audioContext.sampleRate*2*freq.length));
let avg = band.reduce((a,b) => a+b,0)/band.length;
context.fillStyle = `rgb(
${Math.floor(255 - avg)},
0,
${Math.floor(avg)})`;
context.fillRect(0,0,200,200);
requestAnimationFrame(render.bind(gl));
}
Из всего спектра выбирается полоса, которая будет представлена одним браузером, и усредняется, получается амплитуда.
Затем мы просто делаем цвет между красным и синим, в зависимости от одной и той же амплитуды.
Давайте нарисуем контур.
Результат использования плода фантазии выглядит примерно так: Теги: #JavaScript #Ненормальное программирование #webrtc #webaudio API
-
Nginx — Создание Собственного Летитбита
19 Oct, 24 -
Спросите Итана №72: История Жизни Вселенной
19 Oct, 24 -
Продажа Исходящего Трафика
19 Oct, 24