kurento 6.14.0文档翻译 第六章(第四部分)计算机视觉例子(JAVA版) 教程
书接上回:kurento 6.14.0文档翻译 第六章(第三部分)Node.js版 教程
这个web应用包括一个WebRTC环回视频通信,添加了一个有趣的帽子到检测到人脸,这个例子是一个视觉例子和增强现实。
6.2.1Java-WebRTC魔镜
这个web应用继承这个HelloWorld教程,添加一个媒体处理基于WebRTC回环
提示:这个教程已经配置了https,跟道这个教程(file:///features/security.html#configure-java-applications-to-use-https)
你可以配置你的加密应用
对于不耐烦:运行这个例子
首先,你应该在运行这个演示程序前安装kurento媒体服务,请访问安装教程(ps:第四章)了解更多信息。
运行这个应用,你需要克隆这个github项目,当你运行在本主机
git clone https://github.com/Kurento/kurento-tutorial-java.git cd kurento-tutorial-java/kurento-magic-mirror git checkout master mvn -U clean spring-boot:run |
这个web应用在端口8443本主机启动,因此在一个符合webrtc浏览器(chrome,firefox)打开这个URL“https://localhost:8443/”。
提示:仅当Kurento Media Server在与本教程相同的机器上启动并运行时,这些说明才有效,你也可以连接远程KMS,可以添加kms.url到这个JVM执行中我们也会用maven,你应该执行以下命令:
mvn -U clean spring-boot:run \ -Dspring-boot.run.jvmArguments="-Dkms.url=ws://{KMS_HOST}:8888/kurento" |
理解这个例子:
这个应用计算机视觉和增强现实技术去添加一个有趣的帽子到脸部上方,以下图片显示浏览器的演示程序截图。
这个应用接口(一个HTML web页面)由两个html5 video标签,一个为了摄像视频流(本地客户流)另一个是镜像(远程流),这个摄像视频流发送到KMS服务,处理并作为远程流将其发送回客户端,实现这个,我们需要创建一个媒体管道由以下媒体元素组成。
• WebRtcEndpoint: 提供全工(双向)WebRTC 能力。
FaceOverlay 过滤器: 计算机视觉过滤器侦测到视频流中存在脸,就把一个图片放到这个脸的上方,这个演示程序过滤器配置了一个超级马里奥帽子。
这个web应用,是一个C/S架构,在客户端实现逻辑是javascript,在服务器我们使用了基于spring-boot应用服务使用Kurento Java客户端API去控制kurnto媒体服务,此演示的高层架构是三层的去传输这些实体,两个websocket被使用了,第一个websocket创建客户端与应用服务去实现通用的信令协议,第二个websocket 用于在Kurento Java客户端和Kurento媒体服务器之间执行通信。这个通信使用kurento协议,更多相关的信息可以看第16章相应的文档。
客户端与应用服务相互通信是在websocket之上传输JSON消息,正常的客户端与服务之前的序列是1)客户端开始这个魔镜2)客户端结束这个魔镜
如果有任何异常发送,服务发送一个错误信息到客户端,客户端和应用程序服务器之间的详细消息序列如下图所示:
从这个图你可以看到,在Kurento客户端和服务器之间建立WebRTC会话中需要在客户端和服务器之间交换SDP和ICE候选对象,。特别,这个SDP协商连接浏览器的webrtcpeer和服务器的webrtcendpoint,完整的原码可以在github上找到(https://github.com/Kurento/kurento-tutorial-java/tree/master/kurento-magic-mirror)。
应用服务方面
这个演示程序服务器用java语言开发,基于spring-boot框架,嵌入一个Tomcat服务,这样可以更简单的开发和部署
提示:你可以用其它的java技术服务器,比如javaee,sip servlet,play,Vert.x等,我们选择了spring-boot
以下图形你可以看到服务器的相关类图
主类的名称叫 MagicMirrorApp,如你所见,这个kurentoClient在这个类以spring bean的形式实例化,这个bean用于创建kurento媒体管道,用于在你的应用添加媒体能力,在这个实例化我们看到我们需要在本地客户端的库指明kurento媒体服务位置,在这个例子中我们假设它是本地主机,监听8888,如果你重新设置了你需要在这插入指明的kurento媒体服务的位置。
@EnableWebSocket @SpringBootApplication public class MagicMirrorApp implements WebSocketConfigurer { final static String DEFAULT_KMS_WS_URI = "ws://localhost:8888/kurento"; @Bean public MagicMirrorHandler handler() { return new MagicMirrorHandler(); } @Bean public KurentoClient kurentoClient() { return KurentoClient.create(); } @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(handler(), "/magicmirror"); } public static void main(String[] args) throws Exception { new SpringApplication(MagicMirrorApp.class).run(args); } } |
|
这个web应用是一个单页面(SPA)架构,客户端和应用服务使用了一个websocket进行通信,特别这个主App类实现了 WebSocketConfigurer接口去注册一个WebSocketHandler去处理/magicmirror路径的websocket请求。
MagicMirrorHandler 类实现了TextWebSocketHandler去处理文本的websocket请求,这个类的中间handlerTextMessage方法,此方法实现请求的操作,并通过WebSocket返回响应。换句话说,它实现了服务的信令部分
这个协议设计中有三种不同的消息到服务中,start,stop和oniceCadidates,这些消息在switch中处理,在每种情况下都应采取适当的步骤。
public class MagicMirrorHandler extends TextWebSocketHandler { private final Logger log = LoggerFactory.getLogger(MagicMirrorHandler.class); private static final Gson gson = new GsonBuilder().create(); private final ConcurrentHashMap<String, UserSession> users = new ConcurrentHashMap ˓→<String, UserSession>(); @Autowired private KurentoClient kurento; @Override public void handleTextMessage(WebSocketSession session, TextMessage message) ˓→throws Exception { JsonObject jsonMessage = gson.fromJson(message.getPayload(), JsonObject.class); log.debug("Incoming message: {}", jsonMessage); switch (jsonMessage.get("id").getAsString()) { case "start": start(session, jsonMessage); break; case "stop": { UserSession user = users.remove(session.getId()); if (user != null) { user.release(); } break; } case "onIceCandidate": { JsonObject jsonCandidate = jsonMessage.get("candidate").getAsJsonObject(); UserSession user = users.get(session.getId()); if (user != null) { IceCandidate candidate = new IceCandidate(jsonCandidate.get("candidate"). ˓→getAsString(), jsonCandidate.get("sdpMid").getAsString(), jsonCandidate.get( ˓→"sdpMLineIndex").getAsInt()); user.addCandidate(candidate); } break; } default: sendError(session, "Invalid message with id " + jsonMessage.get("id"). ˓→getAsString()); break; } } private void start(WebSocketSession session, JsonObject jsonMessage) { ... } private void sendError(WebSocketSession session, String message) { ... } } |
接下来的片段,我们能看到start方法,它处理这个ICE候选人聚集,创建一个媒体管道,创建媒体元素(WebRtcEndpoint 和 FaceOverlayFilt er),使它们之间取得联系,一个startResponse消息从客户端返回SDP回应。
private void start(final WebSocketSession session, JsonObject jsonMessage) { try { // User session UserSession user = new UserSession(); MediaPipeline pipeline = kurento.createMediaPipeline(); user.setMediaPipeline(pipeline); WebRtcEndpoint webRtcEndpoint = new WebRtcEndpoint.Builder(pipeline).build(); user.setWebRtcEndpoint(webRtcEndpoint); users.put(session.getId(), user); // ICE candidates webRtcEndpoint.addIceCandidateFoundListener(new EventListener ˓→<IceCandidateFoundEvent>() { @Override public void onEvent(IceCandidateFoundEvent event) { JsonObject response = new JsonObject(); response.addProperty("id", "iceCandidate"); response.add("candidate", JsonUtils.toJsonObject(event.getCandidate())); try { synchronized (session) { session.sendMessage(new TextMessage(response.toString())); } } catch (IOException e) { log.debug(e.getMessage()); } } }); // Media logic FaceOverlayFilter faceOverlayFilter = new FaceOverlayFilter.Builder(pipeline). ˓→build(); String appServerUrl = System.getProperty("app.server.url", MagicMirrorApp. ˓→DEFAULT_APP_SERVER_URL); faceOverlayFilter.setOverlayedImage(appServerUrl + "/img/mario-wings.png", -0. ˓→35F, -1.2F, 1.6F, 1.6F); webRtcEndpoint.connect(faceOverlayFilter); faceOverlayFilter.connect(webRtcEndpoint); // SDP negotiation (offer and answer) String sdpOffer = jsonMessage.get("sdpOffer").getAsString(); String sdpAnswer = webRtcEndpoint.processOffer(sdpOffer); JsonObject response = new JsonObject(); response.addProperty("id", "startResponse"); response.addProperty("sdpAnswer", sdpAnswer); synchronized (session) { session.sendMessage(new TextMessage(response.toString())); } webRtcEndpoint.gatherCandidates(); } catch (Throwable t) { sendError(session, t.getMessage()); } } |
提示:注意帽子URL由应用程序服务器提供由KMS使用,这个逻辑假设应用服务是本地主机(localhost)默认的帽子URL是 https://localhost:8443/img/ mario-wings.png,如果你的应用服务是不同的主机上,它也可以很容的修改配置参数app.server.url,比如
mvn -U clean spring-boot:run -Dapp.server.url=https://app_server_host:app_server_port |
这个sendError方法是十分简单的,它发送一个错误信息到客户端当服务器发送异常时。
private void sendError(WebSocketSession session, String message) { try { JsonObject response = new JsonObject(); response.addProperty("id", "error"); response.addProperty("message", message); session.sendMessage(new TextMessage(response.toString())); } catch (IOException e) { log.error("Exception sending message", e); } } |
客户端方面:
让我们看一下客户端应用程序,在服务器调用以前创建的websocket,我们用javascript类websocket,我们用一个具体的kurento javascript库叫着kurnto-utils.js
去简化WebRTC与服务器的交互,这个库基于adapter.js, 这是Google维护的JavaScript WebRTC实用程序,可抽象化浏览器差异,最后jquery.js也需在这个应用程序内。
这些库都在index.html内引接,方法使用的index.js,接下来的片段我们能看到创建websocket(变量名ws)在路径为/magicmirror,然后onmessage监听websocket使用了实现json信令协议的客户端,注意这有三个消息到客户端:startResponse,error,和iceCandidate,采取了方便的措施来实现通信中的每个步骤,例如在方法start内使用了kurento-utils.js文件webRtcPeer、WebRtcPeerSendrecv方法用于webrtc通信。
var ws = new WebSocket('ws://' + location.host + '/magicmirror'); ws.onmessage = function(message) { var parsedMessage = JSON.parse(message.data); console.info('Received message: ' + message.data); switch (parsedMessage.id) { case 'startResponse': startResponse(parsedMessage); break; case 'error': if (state == I_AM_STARTING) { setState(I_CAN_START); } onError("Error message from server: " + parsedMessage.message); break; case 'iceCandidate': webRtcPeer.addIceCandidate(parsedMessage.candidate, function (error) { if (error) { console.error("Error adding candidate: " + error); return; } }); break; default: if (state == I_AM_STARTING) { setState(I_CAN_START); } onError('Unrecognized message', parsedMessage); } } function start() { console.log("Starting video call ...") // Disable start button setState(I_AM_STARTING); showSpinner(videoInput, videoOutput); console.log("Creating WebRtcPeer and generating local sdp offer ..."); var options = { localVideo: videoInput, remoteVideo: videoOutput, onicecandidate: onIceCandidate } webRtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv(options, function (error) { if (error) { return console.error(error); } webRtcPeer.generateOffer(onOffer); }); } function onOffer(offerSdp) { console.info('Invoking SDP offer callback function ' + location.host); var message = { id : 'start', sdpOffer : offerSdp } sendMessage(message); } function onIceCandidate(candidate) { console.log("Local candidate" + JSON.stringify(candidate)); var message = { id: 'onIceCandidate', candidate: candidate }; sendMessage(message); }c |
依赖
这个java spring应用使用maven, pom.xml的相关部分是声明Kurento依赖项的地方,c以下片段显示,我们需要两个依赖,Kurento客户端Java依赖项(kurento-client)和
客户端的javaScript Kurento实用程序库(kurento-utils)。 其他客户端库通过webjar进行管理:
<dependencies> <dependency> <groupId>org.kurento</groupId> <artifactId>kurento-client</artifactId> </dependency> <dependency> <groupId>org.kurento</groupId> <artifactId>kurento-utils-js</artifactId> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator</artifactId> </dependency> <dependency> <groupId>org.webjars.bower</groupId> <artifactId>bootstrap</artifactId> </dependency> <dependency> <groupId>org.webjars.bower</groupId> <artifactId>demo-console</artifactId> </dependency> <dependency> <groupId>org.webjars.bower</groupId> <artifactId>adapter.js</artifactId> </dependency> <dependency> <groupId>org.webjars.bower</groupId> <artifactId>jquery</artifactId> </dependency> <dependency> <groupId>org.webjars.bower</groupId> <artifactId>ekko-lightbox</artifactId> </dependency> </dependencies> |
提示:我们正在积级发展,我们能在maven中心找到kurento最新版本java客户端
Kurento java客户端必须要不小于jdk7,我们需要添加以下两行到你的pom文件中
<maven.compiler.target>1.7</maven.compiler.target> <maven.compiler.source>1.7</maven.compiler.source> |