Статьи

Двоичные веб-сокеты с Play 2.0 и Scala

В недавней статье я показал, как вы можете использовать webrtc, canvas и websockets вместе, чтобы создать приложение для обнаружения лиц, внешний интерфейс которого полностью работает в браузере без использования плагинов. В этой статье я использовал основанный на Jetty сервер для анализа изображений с использованием OpenCV через оболочку JavaCV.

Когда я почти закончил статью, я заметил, что веб-розетки также поддерживаются в Play 2.0. Мне очень нравится разработка в Play и в Scala, поэтому в качестве эксперимента я переписал внутреннюю часть стека Jetty / Java / JavaCV в стек Play2.0 / Scala / JavaCV. Если вы хотите сделать это для себя, убедитесь, что вы начинаете с кода внешнего интерфейса здесь . Так как код внешнего интерфейса не изменился, кроме места, где прослушиваются веб-сокеты.

Настройка среды Play 2.0

Я не буду много говорить о том, как начать проект Play 2.0 / Scala. Вы можете найти подробности в некоторых других моих постах, если вам потребуется дополнительная информация. Нам нужно настроить зависимости для JavaCV, чтобы их можно было использовать из Play 2. Я вручную добавил их в свой локальный репозиторий ivy, чтобы я мог ссылаться на них из конфигурации sbt, как и любая другая зависимость. Для моего примера я создал следующую структуру каталогов для библиотек JavaCV:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
./play-2.0-RC2/repository/cache/javacv
./play-2.0-RC2/repository/cache/javacv/javacpp
./play-2.0-RC2/repository/cache/javacv/javacpp/ivy-2.3.1.xml
./play-2.0-RC2/repository/cache/javacv/javacpp/ivydata-2.3.1.properties
./play-2.0-RC2/repository/cache/javacv/javacv
./play-2.0-RC2/repository/cache/javacv/javacv/ivy-2.3.1.xml
./play-2.0-RC2/repository/cache/javacv/javacv/ivydata-2.3.1.properties
./play-2.0-RC2/repository/cache/javacv/javacv-macosx-x86_64
./play-2.0-RC2/repository/cache/javacv/javacv-macosx-x86_64/ivy-2.3.1.xml
./play-2.0-RC2/repository/cache/javacv/javacv-macosx-x86_64/ivydata-2.3.1.properties
./play-2.0-RC2/repository/local/javacv
./play-2.0-RC2/repository/local/javacv/javacpp
./play-2.0-RC2/repository/local/javacv/javacpp/2.3.1
./play-2.0-RC2/repository/local/javacv/javacpp/2.3.1/ivys
./play-2.0-RC2/repository/local/javacv/javacpp/2.3.1/ivys/ivy.xml
./play-2.0-RC2/repository/local/javacv/javacpp/2.3.1/jars
./play-2.0-RC2/repository/local/javacv/javacpp/2.3.1/jars/javacpp.jar
./play-2.0-RC2/repository/local/javacv/javacv
./play-2.0-RC2/repository/local/javacv/javacv/2.3.1
./play-2.0-RC2/repository/local/javacv/javacv/2.3.1/ivys
./play-2.0-RC2/repository/local/javacv/javacv/2.3.1/ivys/ivy.xml
./play-2.0-RC2/repository/local/javacv/javacv/2.3.1/jars
./play-2.0-RC2/repository/local/javacv/javacv/2.3.1/jars/javacv.jar
./play-2.0-RC2/repository/local/javacv/javacv-macosx-x86_64
./play-2.0-RC2/repository/local/javacv/javacv-macosx-x86_64/2.3.1
./play-2.0-RC2/repository/local/javacv/javacv-macosx-x86_64/2.3.1/ivys
./play-2.0-RC2/repository/local/javacv/javacv-macosx-x86_64/2.3.1/ivys/ivy.xml
./play-2.0-RC2/repository/local/javacv/javacv-macosx-x86_64/2.3.1/jars
./play-2.0-RC2/repository/local/javacv/javacv-macosx-x86_64/2.3.1/jars/javacv-macosx-x86_64.jar

Как вы можете видеть из этого списка, я только что добавил три javacv-файла в мой локальный репозиторий. Я также добавил минимальный файл ivy.xml, чтобы его можно было использовать из ivy и sbt. Этот минимальный ivy.xml выглядит так:

01
02
03
04
05
06
07
08
09
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<ivy-module version="2.0" xmlns:m="http://ant.apache.org/ivy/maven">
    <info
        organisation="javacv"
        module="javacv-macosx-x86_64"
        revision="2.3.1"
        status="release">
        </info>
 
        <publications>
            <artifact type="jar"/>
        </publications>
</ivy-module>

С этими файлами, добавленными в мой репозиторий, я могу установить зависимости для проекта Play 2.0 в файле Build.scala.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
import sbt._
import Keys._
import PlayProject._
 
object ApplicationBuild extends Build {
 
    val appName         = "PlayWebsocketJavaCV"
    val appVersion      = "1.0-SNAPSHOT"
 
    val appDependencies = Seq(
      "javacv" % "javacv" % "2.3.1",
      "javacv" % "javacpp" % "2.3.1",
      "javacv" % "javacv-macosx-x86_64" % "2.3.1"
    )
 
    val main = PlayProject(appName, appVersion, appDependencies, mainLang = SCALA).settings(
      // Add your own project settings here     
    )
}

Теперь запустите «play update» и «play eclipsify», чтобы обновить зависимости и конфигурацию Eclipse (если вы используете Eclipse).

Настройка веб-сокетов в Play

Использование веб-сокетов в Play 2.0 очень просто. Первое, что вам нужно сделать, это добавить URL к вашей конфигурации маршрутов в каталоге conf.

1
GET     /wsrequest                  controllers.Application.wsrequest

И, конечно же, вам нужно выполнить действие, на которое указывает этот маршрут:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
/**
 * Simple websocket listener configured in play 2. This uses a synchronous model, where
 * the same channel is used to send the response. For this usecase this is useful, if
 * we want async processing we could have used Akka actors together with play 2.0 async
 * support.
 */
def wsrequest = WebSocket.using[Array[Byte]] { request =>
 
  // Create the outbound value that is called for each
  val out = Enumerator.imperative[Array[Byte]]();
 
 val in = Iteratee.foreach[Array[Byte]](content => {
    out.push(FaceDetect.detect(content));
  })
 
  // tie the in and out values to each other
  (in, out)
}

В этом коде мы настраиваем входной канал (вход) и выходной канал (выход) и подключаем их к сокету. Всякий раз, когда клиент HTML5 отправляет запрос через веб-сокет, вызывается наш метод «in», и когда мы хотим что-то отправить клиенту, мы можем использовать канал «out». Канал «in» должен быть определен как Iteratee (более подробную информацию см. В этих документах Play). Это означает, что для каждого входного сообщения, которое мы получаем, мы запускаем метод specifici. В этом случае мы запускаем операцию FaceDetect.detect (подробнее об этом позже), и результат этой операции передается обратно клиенту через канал «out». Этот «выходной» канал сам по себе определен как Enumerator (см. Эти документы для воспроизведения). Мы можем присоединить разных слушателей, если хотим к этому перечислителю, но в этом случае мы ничего не делаем с сообщением, просто передаем его клиенту.

Использование JavaCV из Scala

Последний шаг — это код функции FaceDetect.detect. Версия Java, см. Ранее упомянутую статью , очень легко конвертируется в версию Scala.

01
02
03
04
05
06
07
08
09
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
package javacv
 
import com.googlecode.javacv.cpp.opencv_core._
import com.googlecode.javacv.cpp.opencv_imgproc._
import com.googlecode.javacv.cpp.opencv_highgui._
import com.googlecode.javacv.cpp.opencv_objdetect._
import com.googlecode.javacpp.BytePointer
import java.nio.ByteBuffer
import javax.imageio.ImageIO
import java.io.ByteArrayOutputStream
import scala.tools.nsc.io.VirtualFile
 
object FaceDetect {
 
 var CASCADE_FILE =PATH_TO_CASCADE_FILE;
   var minsize = 20;
 var group = 0;
 var scale = 1.1;
 
  def detect(imageData:Array[Byte]) : Array[Byte] = {
 
    // we need to wrap the input array, since BytePointer doesn't accept
    // a bytearray as input. It accepts a byte varargs, but Array[Byte]
    // doesn't convert automatically
    var wrappedData = ByteBuffer.wrap(imageData);
    var originalImage = cvDecodeImage(cvMat(1, imageData.length,CV_8UC1, new BytePointer(wrappedData)));
 
    // convert to grayscale for easy detection
    var grayImage = IplImage.create(originalImage.width(), originalImage.height(), IPL_DEPTH_8U, 1);
    cvCvtColor(originalImage, grayImage, CV_BGR2GRAY);
 
    // storage is needed to store information during detection
 var storage = CvMemStorage.create();
 
 // load and run the cascade
 var cascade = new CvHaarClassifierCascade(cvLoad(CASCADE_FILE));
 var faces = cvHaarDetectObjects(grayImage, cascade, storage, scale, group, minsize);
 
 // draw a rectangle for the detected faces
 for (i <- 0 until faces.total) {
   var r = new CvRect(cvGetSeqElem(faces, i));
   cvRectangle(originalImage, cvPoint(r.x, r.y),
     cvPoint(r.x + r.width(), r.y + r.height),
     CvScalar.YELLOW, 1, CV_AA, 0);
 }
 
 // convert to bytearray and return
 var bout = new ByteArrayOutputStream();
 var imgb = originalImage.getBufferedImage();
 ImageIO.write(imgb, "png", bout);
 
    bout.toByteArray()
  }
}

Единственная проблема, с которой я столкнулся, была с конструктором BytePointer. Одна из подписей принимает переменные типа byte. В Java это позволяет мне просто предоставить этому конструктору байт [], но в Scala это не работает. К счастью, BytePointer также может быть создан с использованием ByteBuffer. В остальном это преобразование Java в Scala.

Выполнение кода

И это почти все. По умолчанию play прослушивает порт 9000, в примере на базе Jetty сервер работал на 9999 и слушал корневой контекст. Для работы с нашим сервером на базе Play2 / Scala нам просто нужно указать браузеру правильный URL-адрес сервера веб-сокетов.

1
ws = new WebSocket("ws://127.0.0.1:9000/wsrequest");

И теперь, когда мы запускаем его, мы используем Play 2 в качестве нашего сервера и запускаем код JavaCV с помощью scal. И что еще более важно это все еще работает:

Ссылка: бинарные веб-сокеты с Play 2.0 и Scala (и чуть более JavaCV / OpenCV) от нашего партнера JCG Йоса Дирксена в блоге Smart Java .