Главная > Soft > Вещание с web-камеры через websocket, реализация на java

Вещание с web-камеры через websocket, реализация на java

После эксперимента с получением картинки с помощью Java и OpenCV пришла идея организовать вещание видео на сайте своей локальной сети. Но как это проще сделать?

Сейчас Javascript достиг того уровня, когда можно не ограничиваться простой перезагрузкой картинки на странице, а воспользоваться другими более удобными методами. Мой выбор пал на относительно молодую технологию WebSockets. Она позволяет организовать duplex соединение клиент-сервер, что уменьшает потери времени на установление подключения.

Последние версии Application и Web серверов для java приложений уже поддерживают спецификацию JSR356, которая определяет стандартный интерфейс для WebSocket. Поэтому в данном примере будут очень уместно этим воспользоваться на примере Jetty

 Архитектура примера будет следующей:

Вещание с web-камеры через websocket, реализация на java

Камера будет подключена к локальному серверу, который с помощью OpenCV будет забирать картинки и через WebSocket передавать в браузер.

Для начала нужно подготовить класс для работы с OpenCV. Нам нужен некий Singleton, который будет просто отдавать картинки в серверный WebSocket. В браузере картинки будут использоваться в виде Embedded image, поэтому и передавать их лучше в Base64:

<img alt="image" src="data:image/png;base64,iVBORUgAAADIA..." />

У меня получился следующий helper, который умеет сразу отдавать картинку в Base64:

package info.privateblog.opencv;

import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;

import javax.imageio.ImageIO;

import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.videoio.VideoCapture;
import org.opencv.videoio.Videoio;

public class OpenCVHelper {
	private static OpenCVHelper instance = new OpenCVHelper();
	public static OpenCVHelper getInstance() {
		return instance;
	}
	
    private VideoCapture camera = null;  
	private OpenCVHelper() {
		System.loadLibrary(Core.NATIVE_LIBRARY_NAME);  
		camera = new VideoCapture(0);
		camera.set(Videoio.CV_CAP_PROP_FRAME_WIDTH, 640);  
        camera.set(Videoio.CV_CAP_PROP_FRAME_HEIGHT, 480); 
        if(!camera.isOpened()){  
        	throw new IllegalStateException("Cannot open camera");
        }  
	}
	
	public BufferedImage getBufferedImage() {
        Mat frame = new Mat();  
        if (camera.read(frame)){  
        	return convertMatToBufferedImage(frame);
        }
        return null;
	}
	
	public String getBase64Image() {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		BufferedImage image = this.getBufferedImage();
		if (image != null) {
	        try {
	        	ImageIO.write( image, "jpg", baos);
	        	return Base64.getEncoder().encodeToString(baos.toByteArray());
	        } catch( IOException ioe ) {
	        	ioe.printStackTrace();
	        }		
		}
		return null;
	}
	
	private static BufferedImage convertMatToBufferedImage(Mat mat) {  
		byte[] data = new byte[mat.width() * mat.height() * (int)mat.elemSize()];  
		int type;  
		mat.get(0, 0, data);  
		switch (mat.channels()) {    
			case 1:    
				type = BufferedImage.TYPE_BYTE_GRAY;    
			break;    
			case 3:    
				type = BufferedImage.TYPE_3BYTE_BGR;    
			byte b;    
				for(int i=0; i<data.length; i=i+3) {    
					b = data[i];    
					data[i] = data[i+2];    
					data[i+2] = b;    
				}    
			break;    
			default:    
				throw new IllegalStateException("Unsupported number of channels");  
		}    
		BufferedImage out = new BufferedImage(mat.width(), mat.height(), type);  
		out.getRaster().setDataElements(0, 0, mat.width(), mat.height(), data);  
		return out;  
	}  
}

Картинка отдаётся из OpenCV в Mat-объекте, который конвертируется в BufferedImage, сохраняется как jpg и преобразуется в массив Base64.

Хоть камера и позволяет использовать разрешение 1280×960, но передача картинок с таким разрешением сильно тормозит. Пришлось ограничиться 640×320.

Теперь нужен класс для серверной части WebSocket:

package info.privateblog.jetty;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

import javax.imageio.ImageIO;
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.OpenCVHelper;

@ServerEndpoint(value = "/image")
public class ImageWebSocket {
	@OnOpen
	public void onSessionOpened(Session session) {
		System.out.println("onSessionOpened: " + session);
	}
	@OnMessage
	public void onMessageReceived(String message, Session session) throws IOException {
		if ("img".equals(message)) {
			String image = OpenCVHelper.getInstance().getBase64Image();
			if (image != null) {
				session.getBasicRemote().sendText(image);
			} else {
				System.out.println("Error: Null image");
			}
		}
	}
	@OnClose
	public void onClose(Session session, CloseReason closeReason){
		System.out.println("onClose: " + session);
	}
	@OnError
	public void onErrorReceived(Throwable t) {
		System.out.println("onErrorReceived: " + t);
	}
}

Тут тоже всё просто. С помощью аннотаций мы помечаем, какие методы будут вызывать в случае подключения браузера или передачи сообщения. Передача картинки начинается на сообщение img из браузера. Можно не ожидать такого сообщения, а просто начинать передачу.

Теперь нам нужен класс, который будет стартовать Web-сервер. Jetty — это гибкий контейнер, поэтому его можно стартовать даже из main метода:

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.ImageWebSocket;

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);
		
		
       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(ImageWebSocket.class);
			
			server.start();
			System.out.println("Listening port : " + port );
	        
			server.join();
		} catch (Exception e) {
			System.out.println("Error.");
			e.printStackTrace();
		}
	}
}

Здесь мы указываем, что сервер будет запускаться на порту 8080 и начальной страницей будет index.html, которую нам надо подготовить:

<!DOCTYPE HTML>
	<html>
	<head>
		<meta http-equiv="Content-type" content="text/html; charset=utf-8">
		<title>WebSocket Test</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 ws = new WebSocket("ws://localhost:8080/image");

					ws.onopen = function () {
					  ws.send('img');
					};

					ws.onerror = function (e) {
					};
				   
					ws.onerror = function (error) {
					};

					var img;
					ws.onmessage = function (e) {
						$("#image").attr('src',  'data:image/jpg;base64,'+e.data);
						ws.send('img');
					}
				}
									   
				document.addEventListener("DOMContentLoaded", ready, false);
		</script>
	</head>
	<body>
		<img id="image" src="" width="640" height="480"/>
	</body>
</html>

Теперь у нас есть всё, чтоб начать вещание.

PS: данное руководство не описывает, как установить Jetty и OpenCV и настроить Japa Project в Eclipse. Надеюсь, это получится сделать самостоятельно.

Categories: Soft Tags: , , , ,
  1. Пока что нет комментариев.
  1. Пока что нет уведомлений.