Преамбула
Вот захотелось Ивану Царевичу видеонаблюдение для государства своего, да так чтобы удалённое!
Чтобы, даже когда он в отпуске на карибах, он знал что в королевстве делается!
Что есть?
- Дешёвая камера Logitech за 500 деревянных
- Компьютер с белым IP
- Прямые руки
Реализация до боли проста: VLC + Приложение на Java
Идея
- VLC снимает поток с камеры и перекодирует его в кодек Theora + OGG и вещает в сеть
- Веб-приложение на Java проксирует трафик до VLC
- Браузер воспроизводит видео в HTML 5 ( В моём случае хорошо его воспроизводит Chrome)
Проксировать траффик казалось бы ненужно, но нет. Нужно! Во-первых — это позволяет распределить камеры по нескольким узлам локальной сети. Во-вторых — это позволяет фильтровать обращения к видео, не всем же мы показывать его будем, вражина не должен видеть что в государстве твориться! Ну а в третьих — позволяет нам иметь один URL для всех камер : http://host:port/stream?camera-id
VLC
Прекрасный плеер с функцией захвата видео и вещания в сеть. Настроить вещание достаточно просто:
Приложение
Для приложения выбран язык Java, маленький веб-сервер Jetty, веб-фреймворк Vaadin.
Самое интересное это фрейм HTML 5 для вещания видео и собственно прокси сервлет на Java.
Показываем видео в HTML 5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset=utf-8> <meta name="viewport" content="width=640"> <style type="text/css"> * { margin: 0; padding: 0; } </style> </head> <body> <section id="wrapper"> <style scoped="scoped"> video { max-width: 640px; margin: 0; } </style> <article> <video id="video" muted loop autoplay> <source src="/videocam-0.1/stream"/> </video> </article> <script> var video = document.getElementById('video'), article = video.parentNode; function init() { navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia; if (navigator.getUserMedia) { // NOTE: at time of writing March 26, 2012, audio isn't working in Chrome navigator.getUserMedia('video', successCallback, errorCallback); // Below is the latest syntax. Using the old syntax for the time being for backwards compatibility. //navigator.getUserMedia({video: true}, successCallback, errorCallback); function successCallback(stream) { window.stream = stream; if (window.webkitURL) { video.src = window.webkitURL.createObjectURL(stream); } else { video.src = stream; } } function errorCallback(error) { console.error('An error occurred: [CODE ' + error.code + ']'); } } } if (navigator.getUserMedia || navigator.webkitGetUserMedia) { init(); } </script> </section> </body> </html> |
Проксируем обращения к потоку видео
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
package ru.jcorp.videocam.stream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.util.*; /** * @author artamonov */ public class VideoStreamServlet extends HttpServlet { private Log log = LogFactory.getLog(getClass()); private Properties streamProperties = new Properties(); @Override public void init() throws ServletException { super.init(); try { streamProperties.load(new InputStreamReader(getClass().getResourceAsStream("/stream.properties"), "UTF-8")); } catch (IOException e) { throw new RuntimeException(e); } } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request, response); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { BufferedInputStream webToProxyBuf = null; BufferedOutputStream proxyToClientBuf; HttpURLConnection connection; log.info("Stream for: " + request.getRemoteAddr()); try { String streamUrl = streamProperties.getProperty("stream.source.url"); String queryString = request.getQueryString(); if (queryString != null) streamUrl += "?" + queryString; connection = prepareConnection(request, streamUrl); connection.connect(); if ("POST".equals(request.getMethod())) { makePost(request, webToProxyBuf, connection); } copyHeadersToOutput(response, connection); webToProxyBuf = new BufferedInputStream(connection.getInputStream()); proxyToClientBuf = new BufferedOutputStream(response.getOutputStream()); copyStream(webToProxyBuf, proxyToClientBuf); proxyToClientBuf.flush(); proxyToClientBuf.close(); webToProxyBuf.close(); connection.disconnect(); } catch (Exception e) { log.warn("Streaming exception", e); } } private void copyHeadersToOutput(HttpServletResponse response, HttpURLConnection connection) throws IOException { response.setStatus(connection.getResponseCode()); Set<Map.Entry<String, List<String>>> headers = connection.getHeaderFields().entrySet(); for (Map.Entry<String, List<String>> entry : headers) { if (entry.getKey() != null) { response.setHeader(entry.getKey(), entry.getValue().get(0)); } } } private void makePost(HttpServletRequest request, BufferedInputStream webToProxyBuf, HttpURLConnection connection) throws IOException { BufferedInputStream clientToProxyBuf = new BufferedInputStream(request.getInputStream()); BufferedOutputStream proxyToWebBuf = new BufferedOutputStream(connection.getOutputStream()); copyStream(webToProxyBuf, proxyToWebBuf); proxyToWebBuf.flush(); proxyToWebBuf.close(); clientToProxyBuf.close(); } private HttpURLConnection prepareConnection(HttpServletRequest request, String streamUrl) throws IOException { HttpURLConnection connection;URL url = new URL(streamUrl); connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod(request.getMethod()); connection.setDoInput(true); connection.setDoOutput(true); connection.setInstanceFollowRedirects(false); connection.setUseCaches(true); Enumeration headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String headerName = headerNames.nextElement().toString(); connection.setRequestProperty(headerName, request.getHeader(headerName)); } return connection; } private void copyStream(InputStream input, OutputStream output) throws IOException { int oneByte; while ((oneByte = input.read()) != -1) output.write(oneByte); } } |
И мы вещаем!
Ну вот теперь, поскольку я использую Vaadin для приложения, можно легко добавить выбор доступных камер, управление доступом и пользователями, и прочие плюшки.
Исходники примера под GPL v3: VideoCamSample
Немножко плюшек на заметку:
- Приложение собирается при помощи Maven 3, минимум телодвижений для сборки War-файла
- Пример написан в Intellij Idea, это позволяет деплоить приложение в Jetty за один клик кнопки Debug
- Vaadin используется не случайно, очень не хотелось писать самому убогую JSP страничку ( он потом пригодиться при наращивании функционала )