Главная > Soft > Вещание звука через websocket, реализация на java

Вещание звука через websocket, реализация на java

В прошлой статье был описан способ вещания с веб-камеры через websocket. Сейчас пришла пора попробовать организовать вещание с микрофона. Архитектура примера будет такой же, как и прошлого:

Вещание с звука через websocket, реализация на java

Сигнал с микрофона поступает прямо на сервер, где он считывается и отправляется через websocket клиенту. В качестве сервера для обработки запросов WebSocket будет использоваться Jetty, поэтому вещание звука хорошо дополняет вещание картинки. Будет достаточно доработать прошлый пример.

Для начала нужно создать Singleton, который будет получать массив byte с сохранённым звуковым файлом внутри. Java хорошо работает с WAVE форматом, который поддерживается современными браузерами, поэтому его мы и будем использовать:

var audio = new Audio(«data:audio/wav;base64,» + e.data);
audio.play();

Такой способ не везде работает, но нас это пока устраивает. Видно, что как и в прошлый раз нам нужно отправлять с сервера файл в кодировке Base64.

На сайте уже был пример про запись звука на Java, им и воспользуемся. Пример описывает запись звука в файл, но хотелось бы получать запись в виде byte массива. Вот тут и кроется дьявол. Хотя функция  AudioSystem.write и может принимать OutputStream для выходных данных, но сделать это с WAVE форматом не получится. WAVE формат требует, чтоб в заголовке файла была указана длинна секции данных, что невозможно сделать в случае с OutputStream. Такая попытка вызова закончится ошибкой:

java.io.IOException: stream length not specified

Проблема решается буферизацией результата и формированием в byte массиве корректного заголовка для WAVE файла.

Второй проблемой является скорость передачи. Если вещать двухканальный несжатый звук, то нагрузка на сеть составит около 300кб в секунду. Нагрузку можно снизить, если вещать одноканальный звук.

package info.privateblog.sound;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Base64;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.TargetDataLine;

import info.privateblog.sound.wave.NewWaveWriter;

public class NewSoundHelper {
	private static NewSoundHelper instance = new NewSoundHelper();
	public static NewSoundHelper getInstance() {
		return instance;
	}
	
	private AudioFormat audioFormat = new AudioFormat(  
							            AudioFormat.Encoding.PCM_SIGNED,  
							            44100.0F, 16, 2, 4, 44100.0F, false);
	private DataLine.Info    info = new DataLine.Info(TargetDataLine.class, audioFormat);  
    private TargetDataLine    targetDataLine = null;  
    private boolean busy = false;
    
	public NewSoundHelper() {
        try {  
            targetDataLine = (TargetDataLine) AudioSystem.getLine(info);  
            targetDataLine.open(audioFormat);  
        } catch (LineUnavailableException e) {  
        	throw new IllegalStateException(e);
        }  
	}
	
	
	public static class SoundRecorder extends Thread    {  
	    private TargetDataLine  m_line;  
	    private AudioInputStream m_audioInputStream;  
	    private NewWaveWriter writer = null;
	    
	    public SoundRecorder(TargetDataLine m_line) {  
	        this.m_line = m_line;  
	        this.m_audioInputStream = new AudioInputStream(m_line);  
	    }  
	  
	    public void start() {  
	        m_line.start();  
	        super.start();  
	    }  
	  
	    public void stopRecording() {  
	        m_line.stop();  
	    }  
	  
	    public void run() {  
	        try {  
	        	writer = new NewWaveWriter(44100);
	        	
	        	byte[]buffer = new byte[256];
	        	int res = 0;
	        	while((res = m_audioInputStream.read(buffer)) > 0) {
	        		writer.write(buffer, 0, res);
	        	}
	        } catch (IOException e) {
	        	System.out.println("Error: " + e.getMessage());
	        }  
	    }  
	    
	    public byte[]getResult() throws IOException {
	    	return writer.getByteBuffer();
	    }
	}
	
	private byte[] getSoundDate() throws IOException {
		targetDataLine.start();  
          
        SoundRecorder j = new SoundRecorder(targetDataLine);  
        j.start();  
        try {
			Thread.currentThread().sleep(150);
		} catch (InterruptedException e) {}  
        j.stopRecording();  

        targetDataLine.stop();

        return j.getResult();
	}
	
	public String getBase64() throws IOException {
		setBusy(true);
		byte[] data = getSoundDate();
		String result = null;
		if (data != null) {
			result = Base64.getEncoder().encodeToString(data);
		}
		setBusy(false);		
		return result;
	}
	
	public synchronized boolean isBusy() {
		return busy;
	}
	
	public synchronized void setBusy(boolean busy) {
		this.busy = busy;
	}
}

Класс для формирования заголовка WAVE файла:

package info.privateblog.sound.wave;

import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class NewWaveWriter {
	private ByteArrayOutputStream outStream = new ByteArrayOutputStream();

    private int mSampleRate;
    private int mChannels;
    private int mSampleBits;

    private int mBytesWritten;

    public NewWaveWriter(int sampleRate) throws IOException {
        this.mSampleRate = sampleRate;
        this.mChannels = 1;//2;
        this.mSampleBits = 16;

        this.mBytesWritten = 0;
        outStream.write(new byte[44]);
    }

    public void write(byte[] src, int offset, int length) throws IOException {
        if (offset > length) {
            throw new IndexOutOfBoundsException(String.format("offset %d is greater than length %d", offset, length));
        }
        for (int i = offset; i < length; i+=4) {
            writeUnsignedShortLE(src[i], src[i+1]);
            if (this.mChannels == 2) {
                writeUnsignedShortLE(src[i + 2], src[i + 3]);
            }
            mBytesWritten += (this.mChannels*2);
        }
    }

    public byte[] getByteBuffer() throws IOException {
    	byte[]result = outStream.toByteArray();
    	writeWaveHeader(result);
    	return result;
    }

    private void writeWaveHeader(byte[]file) throws IOException {
        int bytesPerSec = (mSampleBits + 7) / 8;
        
        int position = 0;
        position = setValue(file, position, "RIFF");
        position = setValue(file, position, mBytesWritten + 36);
        position = setValue(file, position, "WAVE");
        position = setValue(file, position, "fmt ");
        position = setValue(file, position, (int)16);
        position = setValue(file, position, (short) 1);
        position = setValue(file, position, (short) mChannels);
        position = setValue(file, position, mSampleRate);
        position = setValue(file, position, mSampleRate * mChannels * bytesPerSec);
        position = setValue(file, position, (short) (mChannels * bytesPerSec));
        position = setValue(file, position, (short) mSampleBits);
        position = setValue(file, position, "data");
        position = setValue(file, position, mBytesWritten);
    }

    private void writeUnsignedShortLE(byte sample1, byte sample2)
            throws IOException {
    	outStream.write(sample1);
    	outStream.write(sample2);
    }
    
    private static int setValue(byte[]buffer, int position, String value) {
    	for (int i = 0; i< value.length(); i++) {
    		buffer[position + i] = (byte)value.charAt(i);
    	}
    	return position + value.length();
    }
    private static int setValue(byte[]buffer, int position, int value) {
		buffer[position + 3] = (byte)(value>>24);
		buffer[position + 2] = (byte)(value>>16);
		buffer[position + 1] = (byte)(value>>8);
		buffer[position + 0] = (byte)(value);
    	return position + 4;
    }
    private static int setValue(byte[]buffer, int position, short value) {
		buffer[position + 1] = (byte)(value>>8);
		buffer[position] = (byte)value;
    	return position + 2;
    }
}

После того, как у нас есть класс для получения звуковых фрагментов, можно приступить к написанию кода, который и будет осуществлять вещание. Наш обработчик отличается от примера с вещанием картинки- он не требует сообщения от браузера, чтоб начать передачу. Данное улучшение позволяет получить более ровное звучание.

 

package info.privateblog.jetty;

import java.io.IOException;

import javax.websocket.CloseReason;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

import info.privateblog.sound.NewSoundHelper;

@ServerEndpoint(value = "/sound")
public class SoundWebSocket {
	@OnOpen
	public void onSessionOpened(Session session) {
		System.out.println("onSessionOpened: " + session);
		while(session.isOpen()) {
			if(!NewSoundHelper.getInstance().isBusy()) {
				try {
					String result = NewSoundHelper.getInstance().getBase64();
					if (result != null) {
						session.getBasicRemote().sendText(result);
					} else {
						System.out.println("Null value");
					}
				} catch(Exception e) {}
			}

		}
	}
	@OnMessage
	public void onMessageReceived(String message, Session session) throws IOException {
	}
	@OnClose
	public void onClose(Session session, CloseReason closeReason){
		System.out.println("onClose: " + session);
	}
	@OnError
	public void onErrorReceived(Throwable t) {
		System.out.println("onErrorReceived: " + t);
	}
}

И index.html файл, что получить звук

<!DOCTYPE HTML>
	<html>
	<head>
		<meta http-equiv="Content-type" content="text/html; charset=utf-8">
		<title>Sockettester</title>
		<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js" type="text/javascript" charset="utf-8"></script>
		<script type="text/javascript" charset="utf-8">
				function ready() {
					var ws2 = new WebSocket("ws://192.168.100.4:8080/sound");
					ws2.onmessage = function (e) {
						var audio = new Audio("data:audio/wav;base64," + e.data); 
						audio.play();
					}
				}
				document.addEventListener("DOMContentLoaded", ready, false);
			</script>
	</head>
</html>

Теперь осталось подготовить main метод для Jetty и пример готов

package info.privateblog;

import java.net.MalformedURLException;

import javax.websocket.server.ServerContainer;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer;

import info.privateblog.jetty.SoundWebSocket;

public class Started {
	public static void main(String[] args) throws MalformedURLException {
		int port = 8080;

		Server server = new Server(port);

		ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
		context.setContextPath("/");
		context.setResourceBase(".");
		context.setWelcomeFiles(new String[]{ "index.html" });
		server.setHandler(context);
		
		
		// add special pathspec of "/home/" content mapped to the homePath
        ServletHolder holderHome = new ServletHolder("/", DefaultServlet.class);
        holderHome.setInitParameter("dirAllowed","true");
        holderHome.setInitParameter("pathInfoOnly","true");
        context.addServlet(holderHome,"/*");
		
		try {
			ServerContainer wscontainer = WebSocketServerContainerInitializer.configureContext(context);
			wscontainer.addEndpoint(SoundWebSocket.class);
			
			server.start();
			System.out.println("Listening port : " + port );
	        
			server.join();
		} catch (Exception e) {
			System.out.println("Error.");
			e.printStackTrace();
		}
	}
}
Categories: Soft Tags: , , ,
  1. Пока что нет комментариев.
  1. Пока что нет уведомлений.