Menu

  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay
  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay

基于Kurento搭建WebRTC服务器

31
Aug
2017

基于Kurento搭建WebRTC服务器

By Alex
/ in Graphic,Java,JavaScript,Work
/ tags Multimedia, WebRTC, 视频监控
31 Comments
基础

Kurento是一个WebRTC媒体服务器,同时提供了一系列的客户端API,可以简化供浏览器、移动平台使用的视频类应用程序的开发。Kurento支持:

  1. 群组通信(group communications)
  2. 媒体流的转码(transcoding)、录制(recording)、广播(broadcasting)、路由(routing)
  3. 高级媒体处理特性,包括:机器视觉(CV)、视频索引、增强现实(AR)、语音分析

Kurento的模块化架构使其与第三方媒体处理算法 —— 语音识别、人脸识别 —— 很容易集成。

架构

和大部分多媒体通信技术一样,Kurento应用的整体架构包含两个层(layer)或者叫平面(plane):

  1. 信号平面(Signaling Plane):负责通信的管理,例如媒体协商、QoS、呼叫建立、身份验证等
  2. 媒体平面(Media Plane):负责媒体传输、编解码等

典型Kurento应用的整体架构如下图:

kurentoapp-architecture

分层视角

按分层的方式来划分,Kurento应用可以分为三层(类似于典型的Web应用):

  1. 展现层 —— 浏览器、移动应用、其它媒体源等应用客户端:
    1.  基于任意协议和应用逻辑层通信,发起信号处理
    2.  基于RTP/HTTP/WebRTC协议和KMS通信:
      1. 通过KMS的输入端点,传输媒体流到KMS
      2. 通过KMS的输出端点,从KMS获得媒体流
  2. 应用逻辑层——应用服务器负责信号平面:
    1. 基于WebSocket/HTTP/REST/SIP等方式和应用客户端通信,进行信号处理
    2. 内嵌Kurento Client,基于Kurento Protocol与KMS通信,管理媒体元素/媒体管线
  3. 服务层——KMS负责媒体平面,可以对输入流进行各种处理,并产生输出流
层之间的交互

媒体协商(信号处理)阶段:

  1. 客户端首先向应服务器请求某种媒体特性(例如请求一个九画面视频监控流、请求发布自己的SDP)。这块WebRTC没有规定,可以基于任何协议(HTTP/WS/SIP)实现
  2. 应用服务器接收到请求后,执行特定的服务器端逻辑,例如AAA(认证授权审计)、CDR生成等
  3. 应用服务器处理请求,并命令KMS实例化适当的媒体元素、构建媒体流(例如从多个RTSP源混合出九画面)
  4. 媒体流构建完毕后,KMS应答应用服务器,后者应答客户端,告知其如何获取媒体服务

媒体交换阶段:

  1. 客户端利用协商阶段收集的信息,向KMS发起请求(例如向目标端口发起UDP请求,获取九画面视频监控流)

下图是交互的序列示意,注意先后顺序:

kurentoapp-generic_interactions

WebRTC应用的例子

Kurento允许基于WebRTC建立浏览器和KMS之间的实时多媒体会话:

  1. 客户端基于SDP来发布自己的媒体特性,请求发送给应用服务器
  2. 应用服务器根据SDP来创建合适的WebRTC端点,并请求KMS生成一个响应SDP
  3. 应用服务器获得响应SDP后,将其返回给客户端
  4. 由于双方都知道对方的SDP了,客户端和KMS可以进行媒体交换了

下图是交互的序列示意:

kurento-webrtc-session

Kurento也可以作为一个媒体代理,让浏览器之间建立直接的媒体交换。交互序列仍然如上图,仅仅是KMS返回的SDP不同

媒体服务器

WebRTC让浏览器能够进行实时的点对点通信(在没有服务器的情况下)。但是要想实现群组通信、媒体流录制、媒体广播、转码等高级特性,没有媒体服务器是很难实现的。

Kurento的核心是一个媒体服务器(Kurento Media Server,KMS),负责媒体的传输、处理、加载、录制,主要基于 GStreamer实现。此媒体服务器的特性包括:

  1. 网络流协议处理,包括HTTP、RTP、WebRTC
  2. 支持媒体混合(mixing)、路由和分发的群组通信(MCU、SFU功能)
  3. 对机器视觉和增强现实过滤器的一般性支持
  4. 媒体存储支持,支持对WebM、MP4进行录像操作,可以播放任何GStreamer支持的视频格式
  5. 对于GStreamer支持的编码格式,可以进行任意的转码,例如VP8, H.264, H.263, AMR, OPUS, Speex, G.711
模块

KMS基于模块化的设计,模块主要分为三类:

  1. 核心(kms-core)
  2. 媒体元素(kms-elements)
  3. 过滤器(kms-filters)
  4. 其它增强KMS的模块,例如kms-crowddetector, kms-pointerdetector, kms-chroma, kms-platedetector

KMS允许用户扩展自己的模块。

协议

Kurento Protocol是一个网络协议,通过WebSocket暴露KMS的特性。

Kurento API是对上述协议的OO封装,通过此API能够创建媒体元素和管线。Kurento提供了API的Java、JavaScript绑定。

客户端

Kurento提供了Java、JavaScript(包括浏览器和Node.js)的客户端库,通过这些库你可以控制媒体服务器。对于其它编程语言,可以使用 Kurento Protocol协议(基于WebSocket/JSON-RPC)。

Kurento客户端API基于所谓媒体元素(Media Element)的概念。一个每天元素持有一种特定的媒体特性。例如:

  1. 媒体元素WebRtcEndpoint的特性是,接收WebRTC媒体流
  2. 媒体元素RecorderEndpoint的特性是,将接收到的媒体流录制到文件系统
  3. 媒体元素FaceOverlayFilter则能够检测人脸,在其上方显示一个特定的图像

开箱即用的媒体元素如下图:

kurento-basic-toolbox

从开发者角度来说,操控媒体元素就好像搭积木。 你只需要按照期望的拓扑结构把它们连接起来就可以了。一系列连接起来的媒体元素称为媒体管线(Media Pipeline)。只有一个管线内部的媒体元素才能相互通信

当创建管道时,开发者需要明确希望使用到的特性,以及媒体连接(connectivity) —— 产生媒体的元素和消费媒体的元素之间的连接:

Java
1
2
3
sourceMediaElement.connect(sinkMediaElement);
// 例如:客户端接收WebRTC流并录制到媒体服务器的文件系统
webRtcEndpoint.connect(recorderEndpoint);
Web客户端

为了简化浏览器客户端的WebRTC流处理,Kurento提供了工具WebRtcPeer,你仍然可以使用WebRTC的标准API,以及连接到WebRtcEndpoint。

安装配置
安装

你可以在64位Ubuntu 14.04 LTS上安装KMS:

Shell
1
2
3
4
5
6
7
8
9
docker create -it -h kurento --name kurento --network local --dns 172.21.0.1 --ip 172.21.0.6 docker.gmem.cc/ubuntu:14.04 bash
 
# 在上述容器中执行
echo "deb http://ubuntu.kurento.org trusty kms6" | sudo tee /etc/apt/sources.list.d/kurento.list
wget -O - http://ubuntu.kurento.org/kurento.gpg.key | sudo apt-key add -
sudo apt-get update
# 执行下面的命令安装KMS,可能需要手工选择依赖冲突处理方案
aptitude install kurento-media-server-6.0
# 选择降级gcc-4.8-base、libstdc++6的那个方案

要启动或者停止KMS服务,执行下面的命令:

Shell
1
2
3
4
# 启动服务
sudo service kurento-media-server-6.0 start
# 停止服务
sudo service kurento-media-server-6.0 stop
兼容性

Trickle ICE是对ICE的扩展,它允许ICE代理(KMS、客户端)增量的收发candidates而不是交换完整的candidate列表。

由于使用了Trickle ICE协议, 目前的6.0版本的KMS和5.1-版本不兼容,你需要卸载老版本后重新安装:

Shell
1
2
3
sudo apt-get remove kurento-media-server
sudo apt-get purge kurento-media-server
sudo apt-get autoremove

注意sources.list文件和sources.list.d下的文件中,对kurento的引用也要删除。

容器化

在Ubuntu:14.04容器中安装后,关闭容器,提交为镜像:

Shell
1
docker commit kurento docker.gmem.cc/kurento:base

新建一个Docker项目:

Dockerfile
1
2
3
4
5
6
FROM docker.gmem.cc/kurento:base
 
ADD /fs /
RUN chmod +x /entrypoint.sh
 
CMD ["/entrypoint.sh"] 

入口点脚本:

/entrypoint.sh
Shell
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
#!/usr/bin/env bash
 
sighdl() {
    echo
    echo -e "\033[44mKilling sub process $pid \033[0m"
    kill -TERM $pid
    echo -e "\033[44mStopping KMS \033[0m"
    service kurento-media-server-6.0 stop
    echo -e "\033[44mCleaning up log files \033[0m"
    rm -rf /var/log/kurento-media-server/*
}
trap  sighdl HUP INT PIPE QUIT TERM
 
service kurento-media-server-6.0 start
 
sleep 10
 
kmspid=`ps -A | grep kurento-media | xargs |cut -d" " -f1`
pushd /var/log/kurento-media-server > /dev/null
logfile=`find . -name "*pid$kmspid.log" | head -n 1`
 
# 持续输出当前日志的内容,确保容器不退出
tail -f $logfile &
pid=$!
# 捕获到信号的时候,下面的命令退出 —— 等待被中断
wait $pid
 
# 信号处理完毕后,执行下面的命令,如果tail这个子进程已经终止,则wait会立即退出
# 如果子进程正在处理TERM信号,则等待其处理完毕后,wait退出
# 如果没有这个double wait,则子进程有可能成为僵尸,因为没有父进程实际完成wait系统调用
wait $pid

构建新镜像: docker build --force-rm -t docker.gmem.cc/kurento . 

创建基于新镜像容器: 

Shell
1
2
docker create --name kurento -h kurento --dns 172.21.0.1 --network local --ip 172.21.0.6  --expose 8888 \
              -p 8888:8888 docker.gmem.cc/kurento

启动容器:

Shell
1
docker start -i kurento
构建

要自己构建Kurento,可以参考本节的操作步骤。本节记录的操作步骤是在Ubuntu 14.04 TLS上执行的。

构建OpenCV

kms-filters依赖于此库:

Shell
1
2
3
4
5
6
7
8
9
10
11
pushd /home/alex/CPP/lib  > /dev/null
mkdir opencv
pushd opencv > /dev/null
wget https://codeload.github.com/opencv/opencv/zip/2.4.13.3 -O 2.4.13.zip
unzip -o -d . 2.4.13.zip
mv opencv-2.4.13.3 2.4.13
mkdir build
pushd build > /dev/null
cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/home/alex/CPP/lib/opencv/2.4.13 ..
make && make install
popd && popd
构建kurento组件
Shell
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
# Kurento的组件和源码安装在此
export KURENTO_HOME=/home/alex/CPP/lib/kurento
# 构建Kurento组件的通用CMake选项
export CMAKE_OPTS="-DCMAKE_INSTALL_PREFIX:STRING=$KURENTO_HOME -DCMAKE_MODULE_PATH:STRING=$KURENTO_HOME/share/cmake-2.8/Modules"
export BOOST_ROOT=/home/alex/CPP/lib/boost/1.65.1
 
pushd /home/alex/CPP/lib/kurento/src > /dev/null
 
 
# 构建kms-cmake-utils
git clone https://github.com/Kurento/kms-cmake-utils.git
pushd kms-cmake-utils  > /dev/null
mkdir build
pushd build > /dev/null
cmake $CMAKE_OPTS ..
make && make install
popd && popd
 
 
# 构建kurento-module-creator
git clone https://github.com/Kurento/kurento-module-creator.git
pushd kurento-module-creator > /dev/null
mvn package
# CMake模块统一存放处
cp target/classes/FindKurentoModuleCreator.cmake $KURENTO_HOME/share/cmake-2.8/Modules/
mkdir $KURENTO_HOME/kurento-module-creator
cp target/kurento-module-creator-jar-with-dependencies.jar $KURENTO_HOME/kurento-module-creator/
cp scripts/kurento-module-creator $KURENTO_HOME/kurento-module-creator/
export PATH=$PATH:$KURENTO_HOME/kurento-module-creator
popd
 
 
# 构建kms-jsonrpc
git clone https://github.com/Kurento/jsoncpp.git
pushd jsoncpp  > /dev/null
mkdir build
pushd build > /dev/null
# 需要修改$KURENTO_HOME/src/jsoncpp/src/lib_json/CMakeLists.txt
# 添加目标属性 SET_TARGET_PROPERTIES(jsoncpp_lib_static PROPERTIES COMPILE_FLAGS "-fPIC")
# 否则kms-jsonrpc的构建会报错 ...can not be used when making a shared object; recompile with -fPIC
cmake $CMAKE_OPTS ..
make && make install
popd && popd
 
git clone https://github.com/Kurento/kms-jsonrpc.git
pushd kms-jsonrpc > /dev/null
mkdir build
pushd build > /dev/null
# 下一步会报错 package 'kmsjsoncpp>=0.6.0' not found  
# 实际上我们刚刚构建好kmsjsoncpp,其Package config位于$KURENTO_HOME/lib/pkgconfig目录下
export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:$KURENTO_HOME/lib/pkgconfig
cmake $CMAKE_OPTS ..
export LIBRARY_PATH=$LIBRARY_PATH:$KURENTO_HOME/lib
# 报错 fatal error: json/json.h: No such file or directory
# 经过检查,使用的jsoncpp头文件路径是/home/alex/CPP/lib/kurento/include/kmsjsoncpp
# 而实际路径是/home/alex/CPP/lib/kurento/include
# 这是jsoncpp项目的pkgconfig模板错误导致,
# 手工修改$KURENTO_HOME/lib/pkgconfig/kmsjsoncpp.pc最后一行为Cflags: -I${includedir}
make && make install
popd && popd
 
 
# 构建kms-core
# 后续可能需要调试
export CMAKE_OPTS="$CMAKE_OPTS -DCMAKE_BUILD_TYPE:STRING=Debug"
sudo apt install libvpx-dev
# Kurento使用自己打包的gstreamer
echo "deb http://ubuntu.kurento.org trusty kms6" | sudo tee /etc/apt/sources.list.d/kurento.list
wget -O - http://ubuntu.kurento.org/kurento.gpg.key | sudo apt-key add -
sudo apt-get update
sudo apt install libgstreamer1.5-dev libgstreamer-plugins-base1.5-dev
 
git clone https://github.com/Kurento/kms-core.git
pushd kms-core > /dev/null
mkdir build
pushd build > /dev/null
cmake $CMAKE_OPTS ..
# 构建时又找不到BOOST头文件
export CPATH=$CPATH:/home/alex/CPP/lib/boost/1.65.1/include
make && make install
popd && popd
 
 
# 构建kms-elements
export CMAKE_OPTS="$CMAKE_OPTS -DKURENTO_MODULES_DIR:STRING=$KURENTO_HOME/share/kurento/modules"
sudo apt-get install libusrsctp*
git clone https://github.com/Kurento/openwebrtc-gst-plugins.git
pushd openwebrtc-gst-plugins > /dev/null
./autogen.sh
./configure --prefix=$KURENTO_HOME
make && make install
popd
 
sudo apt-get install libnice-dev
 
git clone https://github.com/Kurento/kms-elements.git
pushd kms-elements > /dev/null
mkdir build
pushd build > /dev/null
cmake $CMAKE_OPTS ..
export CPATH=$CPATH:$KURENTO_HOME/include/gstreamer-1.5
make && make install
popd && popd
 
 
# 构建kms-filters
git clone https://github.com/Kurento/kms-filters.git
pushd kms-filters > /dev/null
mkdir build
pushd build > /dev/null
export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/home/alex/CPP/lib/opencv/2.4.13/lib/pkgconfig
export LIBRARY_PATH=$LIBRARY_PATH:/home/alex/CPP/lib/opencv/2.4.13/lib
export CPATH=$CPATH:/home/alex/CPP/lib/opencv/2.4.13/include
# 修改CMake配置/home/alex/CPP/lib/kurento/src/kms-filters/CMakeLists.txt
# 第29-30行,去掉 -Wall -Werror
cmake $CMAKE_OPTS ..
make && make install
popd && popd
 
 
# 构建 kurento-media-server
git clone https://github.com/Kurento/kurento-media-server.git
pushd kurento-media-server > /dev/null
mkdir build
pushd build > /dev/null
cmake $CMAKE_OPTS ..
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/alex/CPP/lib/kurento/lib/x86_64-linux-gnu
make && make install
popd && popd

运行,要运行自己构建的KMS,参考如下脚本:

Shell
1
2
3
4
5
6
#!/bin/bash
cd /home/alex/CPP/lib/kurento/bin
export CLIB_HOME=/home/alex/CPP/lib
export KURENTO_HOME=$CLIB_HOME/kurento
export LD_LIBRARY_PATH=$CLIB_HOME/boost/1.65.1/lib:$KURENTO_HOME/lib:$KURENTO_HOME/lib/x86_64-linux-gnu
./kurento-media-server -f $KURENTO_HOME/etc/kurento/kurento.conf.json -c $KURENTO_HOME/etc/kurento/modules/kurento
配置
配置文件

KMS的主配置文件位于/etc/kurento/kurento.conf.json,内容如下:

/etc/kurento/kurento.conf.json
JavaScript
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
{
  "mediaServer" : {
    "resources": {
        // 当请求创建一个对象时,如果资源用量达到下面的阈值,抛出异常
        "exceptionLimit": "0.8",
        // 如果没有任何存活对象,且资源用量达到下面的阈值,则重启服务器
        "killLimit": "0.7",
        // 垃圾回收器活动间隔(秒)
        "garbageCollectorPeriod": 240
    },
    "net" : {
      // WS用于Kurento Protocol
      "websocket": {
         // 普通WS端口
         "port": 8888,
         // WSS端口、数字证书信息
         "secure": {
            "port": 8433,
            "certificate": "defaultCertificate.pem",
            "password": ""
          },
          "registrar": {
            "address": "ws://localhost:9090",
            "localAddress": "localhost"
          },
        // URL路径
        "path": "kurento",
        "threads": 10
      }
    }
  }
}

此外还有以下配置文件:

媒体元素的一般性参数:/etc/kurento/modules/kurento/MediaElement.conf.json

SDP端点(WebRtcEndpoint、RtpEndpoint)的音视频参数

/etc/kurento/modules/kurento/SdpEndpoint.conf.json

WebRtcEndpoint专有参数:/etc/kurento/modules/kurento/WebRtcEndpoint.conf.json
HttpEndpoint专有参数:/etc/kurento/modules/kurento/HttpEndpoint.conf.json
STUN支持

如果KMS位于NAT设备后面,你需要使用STUN或者TURN以便实现NAT穿透。大部分情况下STUN足够,在对称NAT的情况下才需要使用TURN。

要启用STUN支持,修改配置文件: 

/etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini
INI
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
; 解除注释:
stunServerAddress=<serverAddress>
stunServerPort=<serverPort>
; 公网有很多免费的STUN服务:
; 173.194.66.127:19302
; 173.194.71.127:19302
; 74.125.200.127:19302
; 74.125.204.127:19302
; 173.194.72.127:19302
; 74.125.23.127:3478
; 77.72.174.163:3478
; 77.72.174.165:3478
; 77.72.174.167:3478
; 77.72.174.161:3478
; 208.97.25.20:3478
; 62.71.2.168:3478
; 212.227.67.194:3478
; 212.227.67.195:3478
; 107.23.150.92:3478
; 77.72.169.155:3478
; 77.72.169.156:3478
; 77.72.169.164:3478
; 77.72.169.166:3478
; 77.72.174.162:3478
; 77.72.174.164:3478
; 77.72.174.166:3478
; 77.72.174.160:3478
; 54.172.47.69:3478
TURN支持

要启用TURN支持,解除注释:

/etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini
INI
1
turnURL=user:password@address:port

一个开源的TURN实现是coturn

日志

KMS的日志默认存放在/var/log/kurento-media-server/目录下:

  1. media-server_<timestamp>.<log_number>.<kms_pid>.log为本次运行的KMS日志
  2. media-server_error.log为第三方错误日志
  3. logs子目录存放历史日志
Kurento API

Kurento提供了Java/JavaScript的API,对于其它编程语言,目前需要通过WebSocket/JSON-RPC使用Kurento Protocol。 

本章仅仅进行概念上的阐述,如果需要了解针对具体语言的API,请参阅官方文档:

  1. kurento-client-java:Java客户端
  2. kurento-client-js:JavaScript客户端
  3. kurento-utils-js:用于简化WebRTC应用开发的JavaScript工具
整体结构

Kurento的主要类型的类图如下,可以看到MediaObject是所有类型的根,并且实现了组合模式:

kurento-classes

媒体元素/管线

媒体元素和媒体管线是最核心的API。

媒体元素

MediaElement是媒体流中,执行特定动作的功能单元。它让媒体特性对于应用开发者表现为自包含的黑盒,这些开发者不需要了解底层细节。

MediaElement可以通过mediaSrcs从其它媒体元素接收媒体,或者通过mediaSinks将媒体发送给其它媒体元素。

根据功能的不同,MediaElement可以分为:

  1. 输入媒体元素:支持接收媒体,并将媒体注入到管线中。这类媒体元素有多种,实现从文件、网络、摄像头等来源读取媒体流
  2. 过滤器:能够转换、分析媒体流,实现混合、AR之类的功能
  3. HubPort:Hub负责管理管线中的多个媒体流。每个Hub有多个HubPort,这些HubPort连接其它媒体元素
  4. 输出媒体元素:支持输出媒体,将媒体流带出管线。实现录像、在屏幕上播放、发送到网络等功能

MediaElement常常由Endpoint实现,后者可能同时作为输入、输出元素。

媒体管线

MediaPipeline是MediaElement构成的链条。链条可以有多个作为入口点的输入元素。由一个元素生成的输出流(SRC)可能输入到1-N个元素的输入流(SINK):

pipeline-src-sink

端点

端点是MediaElement的一种实现,能够输入、输出媒体流。端点类层次的类图如下:

kurento-endpoints

这些端点的功能简述如下表:

端点 说明
WebRtcEndpoint 输入输出端点(能够接受外部输入、也能够输出到外部),实现WebRTC协议
RtpEndpoint 输入输出端点,基于SDP进行媒体协商,基于RTP进行流发送
HttpPostEndpoint 输入端点,支持类似于HTTP文件上传那样的POST请求
PlayerEndpoint 输入端点,支持从文件系统、HTTP URL、RTSP URL接收内容,并将其注入到媒体管线中
RecorderEndpoint   输出端点,以可靠的方式存储媒体内容到文件系统。用法示例:
Java
1
2
recorder = new RecorderEndpoint.Builder(pipeline, "录像存储路径").build();
webrtcEndpoint.connect(recorder);

/home/alex/CPP/lib/kurento/src/kurento-media-server

关于端点,要注意:

  1. 这些端点都是在KMS中运行的!尽管你会通过Java/Node的客户端,在应用服务器上操控端点,但是实质上都是基于Kurento协议向KMS发起远程调用
  2. 端点可能具有SRC、SINK端子,分别用于发送媒体流到其它端点、接受其它端点的发来的媒体流。SRC、SINK是媒体管线内部概念
  3. 端点可能对外部系统具有接收、发送媒体流的功能(但不叫SRC/SINK),例如WebRtcEndpoint。接收到的媒体流可以通过SRC发送给其它端点,其它端点发送到SINK的媒体流可以转发到外部系统
  4. 端点自己的SRC可以连接到自己的SINK
WebRtcEndpoint

代表一个运行在KMS中的WebRTC端点,是这类端点的控制接口。WebRTC端点可以和浏览器中的WebRTC客户端交互。例如环回视频流的那个实例,其媒体流向图如下:

kurento-loopback

说明如下:

  1. 摄像头出来视频流,一方面在本地浏览器上渲染
  2. 另外一方面,发送给KMS中的WebRTCEndpoint端点
  3. 上一步的媒体流,到达SRC端子,进而发给自己的SINK端子(环回)
  4. SINK端子的媒体流发回给浏览器
  5. 浏览器在另外一个video元素中渲染视频流

WebRTC端点是P2P的WebRTC通信的一端,另一端可以是使用RTCPeerConnection接口的浏览器、Native的WebRTC应用程序、甚至是另一个KMS服务器。

为了建立WebRTC通信,两端必须进行SDP协商,其中一方作为邀请者(Offerer)另外一方作为应答者(Offeree),WebRTC端点可以作为两种角色之一。

当作为邀请者时:

  1. KMS客户端调用generateOffer()方法后,KMS生成一个SDP offer,此Offer返回给KMS客户端(应用服务器),再被转发给浏览器
  2. 浏览器处理上述Offer,并产生一个应答,应答传递给KMS客户端
  3. 后者调用processAnswer()导致应答转发给KMS

当作为应答者时:

  1. 浏览器生成一个SDP offer,发送到KMS客户端
  2. KMS客户端调用processOffer(),SDP被转发给KMS,KMS生成应答,发送给KMS客户端
  3. KMS客户端把应答转发给浏览器处理

SDP独立于ICE候选发送。Kurento使用优化了的ICE收发机制 ——  Trickle ICE。两端分别、独立的执行收集ICE候选:

  1. 浏览器中候选会自动收集,你可以使用onicecandidate回调接收通知。此事件常常比SDP处理发生的更快
  2. KMS必须依赖于客户端调用gatherCandidates(),并在此调用之前注册IceCandidateFound的监听器

KMS、浏览器每收集到一个ICE候选,就(以KMS客户端也就是应用服务器)为中介,发送给对方。接收到对方的ICE候选后,双方就会开始尝试建立双向连接。

需要注意WebRTC信号处理的异步性,假设你希望录制WebRTC端点的视频,在媒体流实际发送之前就执行录制是没有意义的。要感知WebRTC端点的状态,你需要监听端点的事件:

  1. IceComponentStateChange,在WebRTC点对点连接性发生变化后立即发布。这个事件仅仅能用于检测底层的连接性,处于CONNECTED 状态不意味着媒体流就已经开始传输。连接性状态包括(RFC5245定义了它们之间的状态转换图):
    1. DISCONNECTED  没有任何被调度的活动
    2. GATHERING 开始收集本地(KMS服务器)的ICE候选
    3. /home/alex/CPP/lib/kurento/src/kurento-media-serverCONNECTING  尝试创建连接,这在接收到对方的ICE候选后触发
    4. CONNECTED  至少一个有效的ICE候选对出现,导致双向连接成功
    5. READY  ICE结束,候选对选择完成
    6. FAILED  连接性检查已经完毕,但是媒体流连接没有建立
  2. IceCandidateFound,一旦新的ICE候选可用即触发,这些候选必须被发送给对方
  3. IceGatheringDone,所有ICE候选都被收集完毕后触发
  4. NewCandidatePairSelected,当新的ICE候选对(本地、远程)可用时触发,当媒体会话已经进行后,此事件仍然可以触发 —— 一个更高优先级的ICE候选对被发现时
  5. DataChannelOpen,数据通道打开时
  6. DataChannelClose,数据通道关闭后

流控制、拥塞管理是WebRTC最重要的一项功能。WebRTC连接总是以一个较低的带宽开始,慢慢的加大到最大可用带宽。WebRTC 端点如果服务多个外部连接,那么它们将共享一个码流质量,这意味着一个新的外部连接接入后,现有连接的码流质量会下降(因为要从较低带宽开始)。

默认的带宽范围取值在100kbps-500kbps之间,可以单独设置SRC/SINK、音频/视频的带宽范围:

  1. setMin/MaxVideoRecvBandwidth() 设置接收视频带宽
  2. setMin/MaxAudioRecvBandwidth() 设置接收音频带宽
  3. setMin/MaxVideoSendBandwidth() 设置发送视频带宽

带宽最大值在SDP中有声明。

WebRTC可以提供数据通道,并且支持可靠/不可靠、有序/无序的数据传输。要支持数据通道,必须在创建WebRtcEndpoint时显式说明,默认是不允许创建数据通道的

PlayerEndpoint

此端点从可Seek/不可Seek的媒体源中获取媒体流,并将流注入到KMS中。支持的URL格式:

  1. 挂载到本地文件系统的文件:file:///path/to/file
  2. 提供RTSP协议的摄像头:rtsp://、rtsp://username:password@ip:port...
  3. Web服务器:http(s):///path/to/file、http(s)://username:password@/path/to/file

此端点支持以下操作:

操作 说明
play 开始播放媒体流,可以在pause后调用,恢复播放
stop 停止播放媒体流
pause 暂停播放媒体流
setPosition/getPosition 如果媒体源支持,可以用来执行seek操作
过滤器

这类媒体元素负责媒体的处理、机器视觉、AR等功能。 这些媒体元素的功能简述如下表:

过滤器 说明
ZBarFilter 检测二维码(QR)、条形码,一旦检测成功,就发布一个CodeFoundEvent事件。客户端可以侦听此事件并执行相应的操作
FaceOverlayFilter 检测人脸,叠加一个可配置的图像。用法示例:
Java
1
2
FaceOverlayFilter filter = new FaceOverlayFilter.Builder(pipeline).build();
filter.setOverlayedImage("图片URL", -0.35F, -1.2F, 1.6F, 1.6F);
GStreamerFilter 允许你在Kurento中使用GStreamer过滤器
Hubs

这类媒体对象能够管理多个媒体流。这些媒体对象的功能简述如下表:

Hub 说明
Composite

能够混合多个输入音频流

能够合并多个输入视频流,构成多画面

DispatcherOneToMany 把一个输入HubPort分发给所有输出HubPort
Dispatcher 运行在任意输入-输出HubPort对值之间路由
Kurento Utils JS

Utils JS用于简化浏览器端WebRTC应用的开发。

安装

执行下面的命令安装:

Shell
1
2
3
4
# 基于NPM
npm install kurento-utils
# 基于Bower
bower install kurento-utils

或者到这里下载压缩后的JS文件。

创建连接

WebRtcPeer对RTCPeerConnection进行了包装。连接可以是单向的(进行发送或者接收),也可以是双向的(同时发送接收)。

下面的例子示意了如何基于Utils JS创建一个RTCPeerConnection,并与其它Peer进行会话协商:

JavaScript
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
// 信号处理通道,由你自己决定如何实现,它能够让客户端知道可以和谁通信、如何通信
// 典型的做法是,所有客户端公开一个自己的名字,同时以一条WebSocket连接到服务器
// 客户端通过名字发起通信请求,服务器负责中介会话协商
var signalingChannel = createSignalingChannel(peerName);
 
// 用于显示远程视频的元素
var videoInput = document.getElementById( 'videoInput' );
// 用于显示本地视频的元素
var videoOutput = document.getElementById( 'videoOutput' );
// getUserMedia约束条件
var constraints = {
    audio: true,
    video: {
        width: 640,
        framerate: 15
    }
};
 
var options = {
    localVideo: videoInput,
    remoteVideo: videoOutput,
    onicecandidate: function( candidate ){
        // 把本地candidate发送给Peer,基于Trickle ICE,也就是说,一旦发现一个候选,就立即发送
        // 不等待所有候选收集成功,这样效率更高。此回调可能被调用多次
        signalingChannel.sendCandidate(candidate );
    },
    mediaConstraints: constraints
};
 
// 创建一个连接。注意,在双方都需要创建连接,创建的时机,就是服务器确认了两者要进行通信之后
var webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv( options, function ( error ) {
    // 处理失败
    if ( error ) return onError( error );
    // 生成本地的SDP Offer
    this.generateOffer( onOffer );
} );
 
// 当收到Peer的candidate后,添加。下面的代码应该在信号处理的回调中调用
webRtcPeer.addIceCandidate(candidate);
 
// 当本地SDP Offer生成后,调用此回调
function onOffer( error, sdpOffer ) {
    if ( error ) return onError( error );
    // 发送SDP给Peer,Peer应该给出SDP应答,然后本地调用sdpAnswer回调
    signalingChannel.sendOffer( sdpOffer, sdpAnswer );
    function onAnswer( sdpAnswer ) {
        webRtcPeer.processAnswer( sdpAnswer );
    };
}

简述一下上例对应的业务流程:

  1. 通信发起方A,根据接受方B的标识符,向服务器发送WS请求 —— 我要和B通信
  2. 服务器通过WS推送信息给B,A想和你通信,你愿意吗?
  3. 如果B愿意,服务器通过WS推送消息给A、B,你们可以通信了
  4. A、B分别创建连接对象(WebRtcPeer)
  5. WebRtcPeer会自动收集Candidate,你应该通过WS把Candidate发回服务器,服务器再中转给Peer
  6. 一单A、B都收集到Candidate,它们就有可能进行点对点通信了(如果是局域网内)
  7. A发起(Offer)一个会话描述(SDP),B接收到后,给出Answer
  8. 根据双方的SDP,建立媒体流交换
使用数据通道

数据通道允许你通过活动WebRTC连接传递二进制、文本数据。WebRtcPeer对数据通道的使用也提供了封装,将dataChannels选项设置为true即可使用:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var options = {
    localVideo: videoInput,
    remoteVideo: videoOutput,
    // 启用数据通道
    dataChannels: true,
    // 下面这个配置是可选的,允许你执行一些声明周期回调
    dataChannelConfig: {
        id: getChannelName(),
        onmessage: onMessage,
        onopen: onOpen,
        onclose: onClosed,
        onbufferedamountlow: onbufferedamountlow,
        onerror: onerror
    },
    onicecandidate: onIceCandidate
}
 
webRtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv( options, onWebRtcPeerCreated );

一旦webRtcPeer对象被创建,你就可以调用下面的方法,通过数据通道发送信息:

JavaScript
1
2
// 发送的数据类型取决于应用
webRtcPeer.send('Hello there');

数据通道的生命周期受限于其依赖的连接, webRtcPeer.dispose()被调用后数据通道也被关闭和释放。 

Kurento模块

Kurento是一个可拔插的框架,它的每个插件称为模块。模块分为三类。

主模块

这类模块安装了KMS就可以使用,包括:

模块 说明
kms-core KMS的核心功能,基于C编写
kms-elements 实现媒体元素,例如WebRtcEndpoint、WebRtcEndpoint
kms-filters 实现过滤器,例如FaceOverlayFilter, ZBarFilter, GStreamerFilter
内置模块

这些模块用于增强KMS的基本功能,没有随KMS安装,包括:

模块 说明
kms-pointerdetector

一个过滤器,基于颜色追踪在视频流中检测点(pointers),执行下面的命令安装:

Shell
1
sudo apt-get install kms-pointerdetector-6.0
kms-chroma

一个过滤器,在一个层上让指定的色彩范围变得透明,这样下面层的图像就会显示出来。执行下面的命令安装:

Shell
1
sudo apt-get install kms-chroma-6.0
kms-crowddetector

过滤器,能够检测人群聚集。执行下面的命令安装:

Shell
1
sudo apt-get install kms-crowddetector-6.0
kms-platedetector

过滤器,能够实现车牌检测。执行下面的命令安装:

Shell
1
sudo apt-get install kms-platedetector-6.0
自定义模块

你可以根据需要自己扩展KMS模块。

实例
HelloWorld

这是一个环回视频流的例子 —— 视频流发送给自己,需要一台客户端即可测试。通信流程如下:

  1. 页面加载时,客户端自动创建一个到服务器的wss连接:
    JavaScript
    1
    var ws = new WebSocket('wss://' + location.host + '/helloworld'); 

    信号处理依赖此wss连接进行,信号格式为JSON,其id字段表示消息的类型。

  2. 用户点击页面上的开始按钮,执行下面的逻辑:
    JavaScript
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    var options = {
       // 显示本地流的元素
       localVideo : videoInput,
       // 显示远程流的元素
       remoteVideo : videoOutput,
       // 当候选通信地址可用时,执行的回调
       onicecandidate : onIceCandidate
    }
    // 连接对象创建后执行的回调
    function( err ){
        if ( err ) console.error( err );
        // 生成SDP,成功后执行回调
        webRtcPeer.generateOffer( function( error, offerSdp ) {
            ws.send( JSON.stringify( {
                id : 'start',
                sdpOffer : offerSdp
            } ) );
        });
    }
    // 创建具有收、发能力的连接对象
    webRtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv( options, callback ); 

    也就是说,作为通信发起方:

    1. 创建一个连接对象WebRtcPeerSendrecv,此对象创建后,本地流立即就显示在localVideo这个video标签中
    2. 创建完毕后,即生成SDP,其内容如下(主要是发起方允许的连接方式、支持的媒体特性):
      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
      v=0
      # 第一个数字是会话标识,第二个数字是会话版本。后续三个参数和会话协商无关:网络类型Internet,地址类型IPv4,产生SDP的机器的地址
      o=- 6324724567974172241 2 IN IP4 127.0.0.1
      # 会话的名称,不常用
      s=-
      # 会话起止时间,都为0表示不限制时间
      t=0 0
      # BUNDLE分组将多个媒体行关联起来,在WebRTC中用于在同一RTP会话中传递多个媒体流
      a=group:BUNDLE audio video
      # 在PeerConnection声明周期中,赋予WebRTC媒体流唯一标识
      a=msid-semantic: WMS g8OrPwtoxMptMUo5k12OkvC8opycXELqVG9t
       
      ###  音频行  ###
      # m表示这是一个媒体行,audio表示这是音频,后面是协议,最后的长串数字为媒体格式说明
      m=audio 38968 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 126
      # c表示这是一个连接行,表示收发数据通过什么IP进行。但是由于WebRTC强制使用ICE,因此这一行没什么用
      c=IN IP4 192.168.56.1
      # 明确说明用于RTCP的地址和端口
      a=rtcp:51004 IN IP4 192.168.56.1
      # 下面若干行都是ICE候选,ICE是用于NAT穿透的协议
      #           标识       1RTP/2RTCP 优先级  通信地址和端口
      a=candidate:2999745851 1 udp 2122260223 192.168.56.1 38968 typ host generation 0
      a=candidate:364622241 1 udp 2122194687 10.255.0.1 49487 typ host generation 0
      a=candidate:1051995033 1 udp 2122129151 172.18.0.1 52714 typ host generation 0
      a=candidate:410389623 1 udp 2122063615 172.21.0.1 54819 typ host generation 0
      a=candidate:2199032595 1 udp 2121998079 192.168.1.89 47718 typ host generation 0
      a=candidate:627415207 1 udp 2121932543 192.168.0.89 52455 typ host generation 0
      a=candidate:2999745851 2 udp 2122260222 192.168.56.1 51004 typ host generation 0
      a=candidate:364622241 2 udp 2122194686 10.255.0.1 59954 typ host generation 0
      a=candidate:1051995033 2 udp 2122129150 172.18.0.1 41985 typ host generation 0
      a=candidate:410389623 2 udp 2122063614 172.21.0.1 59234 typ host generation 0
      a=candidate:2199032595 2 udp 2121998078 192.168.1.89 58222 typ host generation 0
      a=candidate:627415207 2 udp 2121932542 192.168.0.89 36590 typ host generation 0
      # 下面两行是ICE参数
      a=ice-ufrag:Oyu3vwR19M1nxsx4
      a=ice-pwd:8RbNWdv799Hz7aXWj2DMIPGH
      # 下面两行是DTLS参数
      # DTLS-SRTP协商时使用的证书的指纹信息
      a=fingerprint:sha-256 58:BC:1A:0B:22:10:95:7B:C9:98:4A:D5:34:E9:44:85:FF:9D:A4:7B:07:39:36:FE:90:59:E0:14:3D:B9:21:6C
      a=setup:actpass
      # 用在BUNDLE中的标识符
      a=mid:audio
      # 定义RTP扩展头
      a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
      a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
      # 同时支持接收、发送
      a=sendrecv
      # 支持RTCP多路复用
      a=rtcp-mux
      # 解码器参数
      a=rtpmap:111 opus/48000/2
      a=rtcp-fb:111 transport-cc
      a=fmtp:111 minptime=10; useinbandfec=1
      a=rtpmap:103 ISAC/16000
      a=rtpmap:104 ISAC/32000
      a=rtpmap:9 G722/8000
      a=rtpmap:0 PCMU/8000
      a=rtpmap:8 PCMA/8000
      a=rtpmap:106 CN/32000
      a=rtpmap:105 CN/16000
      a=rtpmap:13 CN/8000
      a=rtpmap:126 telephone-event/8000
      a=maxptime:60
      # SSRC参数
      a=ssrc:2978616353 cname:GrA29DQMxaUfd99u
      a=ssrc:2978616353 msid:g8OrPwtoxMptMUo5k12OkvC8opycXELqVG9t 97776675-4490-4b74-a849-bbd46a722c89
      a=ssrc:2978616353 mslabel:g8OrPwtoxMptMUo5k12OkvC8opycXELqVG9t
      a=ssrc:2978616353 label:97776675-4490-4b74-a849-bbd46a722c89
      m=video 46497 UDP/TLS/RTP/SAVPF 100 101 116 117 96 97 98
      c=IN IP4 192.168.56.1
      a=rtcp:9 IN IP4 0.0.0.0
      a=candidate:2999745851 1 udp 2122260223 192.168.56.1 46497 typ host generation 0
      a=candidate:364622241 1 udp 2122194687 10.255.0.1 34284 typ host generation 0
      a=ice-ufrag:Oyu3vwR19M1nxsx4
      a=ice-pwd:8RbNWdv799Hz7aXWj2DMIPGH
      a=fingerprint:sha-256 58:BC:1A:0B:22:10:95:7B:C9:98:4A:D5:34:E9:44:85:FF:9D:A4:7B:07:39:36:FE:90:59:E0:14:3D:B9:21:6C
      a=setup:actpass
       
      ### 视频行 ###
      a=mid:video
      a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
      a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
      a=extmap:4 urn:3gpp:video-orientation
      a=sendrecv
      a=rtcp-mux
      # 支持的视频编码
      a=rtpmap:100 VP8/90000
      # 如果客户端是Firefox、Chrome 61 —— 支持H264
      a=rtpmap:120 VP8/90000
      a=rtpmap:126 H264/90000
      a=rtpmap:97 H264/90000
      # 则出现以上三行
      a=rtcp-fb:100 ccm fir
      a=rtcp-fb:100 nack
      a=rtcp-fb:100 nack pli
      a=rtcp-fb:100 goog-remb
      a=rtcp-fb:100 transport-cc
      a=rtpmap:101 VP9/90000
      a=rtcp-fb:101 ccm fir
      a=rtcp-fb:101 nack
      a=rtcp-fb:101 nack pli
      a=rtcp-fb:101 goog-remb
      a=rtcp-fb:101 transport-cc
      a=rtpmap:116 red/90000
      a=rtpmap:117 ulpfec/90000
      a=rtpmap:96 rtx/90000
      a=fmtp:96 apt=100
      a=rtpmap:97 rtx/90000
      a=fmtp:97 apt=101
      a=rtpmap:98 rtx/90000
      a=fmtp:98 apt=116
      a=ssrc-group:FID 3977515695 1979665708
      a=ssrc:3977515695 cname:GrA29DQMxaUfd99u
      a=ssrc:3977515695 msid:g8OrPwtoxMptMUo5k12OkvC8opycXELqVG9t 153f4d5f-ba5b-4772-8700-aff4474d8652
      a=ssrc:3977515695 mslabel:g8OrPwtoxMptMUo5k12OkvC8opycXELqVG9t
      a=ssrc:3977515695 label:153f4d5f-ba5b-4772-8700-aff4474d8652
      a=ssrc:1979665708 cname:GrA29DQMxaUfd99u
      a=ssrc:1979665708 msid:g8OrPwtoxMptMUo5k12OkvC8opycXELqVG9t 153f4d5f-ba5b-4772-8700-aff4474d8652
      a=ssrc:1979665708 mslabel:g8OrPwtoxMptMUo5k12OkvC8opycXELqVG9t
      a=ssrc:1979665708 label:153f4d5f-ba5b-4772-8700-aff4474d8652
    3. 上述SDP以消息类型start发送给服务器
  3. 服务器接收到start消息后,执行以下逻辑:
    Java
    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
    // 创建媒体管线
    MediaPipeline pipeline = kurento.createMediaPipeline();
    // 在管线中添加一个WebRTC端点
    WebRtcEndpoint webRtcEndpoint = new WebRtcEndpoint.Builder(pipeline).build();
    // 连接WebRTC端点到自己
    webRtcEndpoint.connect(webRtcEndpoint);
     
    // 创建一个用户会话(UserSession不属于KMS 客户端API的组成部分)
    UserSession user = new UserSession();
    // 连接到的管线
    user.setMediaPipeline(pipeline);
    // 连接到的端点,注意此端点的输入、输出是同一个流
    user.setWebRtcEndpoint(webRtcEndpoint);
    // 以WebSocket会话标识时别用户https://localhost:8443/#
    users.put(session.getId(), user);
     
    // 处理SDP
    String sdpOffer = jsonMessage.get("sdpOffer").getAsString();
    // 由端点来处理SDP,生成应答
    String sdpAnswer = webRtcEndpoint.processOffer(sdpOffer);
    // 以消息类型startResponse将应答SDP通过WebSocketSession发回给客户端
    JsonObject response = new JsonObject();response.addProperty("id", "startResponse");
    response.addProperty("sdpAnswer", sdpAnswer);
    synchronized (session) {
        session.sendMessage(new TextMessage(response.toString()));
    }
     
    // 一旦收集到服务器的ICE候选信息,即以消息类型iceCandidate发送给客户端
    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.error(e.getMessage());
        }
      }
    });
    // 为某个端点收集服务器的ICE候选信息
    webRtcEndpoint.gatherCandidates();

    服务器生成的SDP应答内容如下:

    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
    v=0
    o=- 3713658153 3713658153 IN IP4 0.0.0.0
    s=Kurento Media Server
    c=IN IP4 0.0.0.0
    t=0 0
    a=msid-semantic: WMS kGkOSxP0iFTu9aRzm53BNz0fROtBq1HxLFje
    a=group:BUNDLE audio video
    m=audio 1 UDP/TLS/RTP/SAVPF 111 0
    a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
    a=mid:audio
    a=rtcp:9 IN IP4 0.0.0.0
    a=rtpmap:111 opus/48000/2
    a=rtpmap:0 PCMU/8000
    a=setup:active
    a=sendrecv
    a=rtcp-mux
    a=fmtp:111 minptime=10; useinbandfec=1
    a=maxptime:60
    a=ssrc:1475810019 cname:user35735626@host-c1cf1e49
    a=ice-ufrag:/Jml
    a=ice-pwd:RCpQ+o7Ybof5B5mxYDGM17
    a=fingerprint:sha-256 B4:72:A8:44:90:3D:CF:1B:8E:30:93:09:AC:66:BF:05:60:D7:0B:C3:C3:AA:28:7D:44:46:8E:55:17:61:4F:43
    m=video 1 UDP/TLS/RTP/SAVPF 100
    a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
    a=mid:video
    a=rtcp:9 IN IP4 0.0.0.0
    # 使用VP8作为视频编码格式
    a=rtpmap:100 VP8/90000
    a=rtcp-fb:100 ccm fir
    a=rtcp-fb:100 nack
    a=rtcp-fb:100 nack pli
    a=rtcp-fb:100 goog-remb
    a=setup:active
    a=sendrecv
    a=rtcp-mux
    a=ssrc:101029323 cname:user35735626@host-c1cf1e49
    a=ice-ufrag:/Jml
    a=ice-pwd:RCpQ+o7Ybof5B5mxYDGM17
    a=fingerprint:sha-256 B4:72:A8:44:90:3D:CF:1B:8E:30:93:09:AC:66:BF:05:60:D7:0B:C3:C3:AA:28:7D:44:46:8E:55:17:61:4F:43
  4. 客户端接收到startResponse消息后,调用下面的方法处理SDP应答:
    JavaScript
    1
    2
    3
    webRtcPeer.processAnswer(message.sdpAnswer, function(error) {
        if (error) console.error(error);
    });
  5. 关于Ice Candidate的处理上面没有提及,这会异步的进行:
    1. 客户端连接创建后,就会自动收集ICE候选,一旦收集到,就调用如下回调:
      JavaScript
      1
      2
      3
      4
      5
      6
      function onIceCandidate(candidate) {
          ws.send(JSON.stringify({
              id : 'onIceCandidate',
              candidate : candidate
          }));
      }

      候选的内容如下:

      JavaScript
      1
      2
      3
      4
      5
      6
      7
      8
      {
          // 此候选的通信地址
          "candidate":"candidate:2999745851 1 udp 2122260223 192.168.56.1 36777 typ host generation 0 ufrag waE0gMnNFX3ug+yW",
          // 此候选关联的媒体流的标识(identification-tag)
          "sdpMid":"audio",
          // 此候选关联SDP中媒体描述的索引
          "sdpMLineIndex":0
      }

      也就是说,以消息类型onIceCandidate发送给服务器

    2. 服务器接收到onIceCandidate消息后,将其保存到用户对象中:
      Java
      1
      2
      3
      4
      5
      6
      7
      UserSession user = users.get(session.getId());
      IceCandidate candidate = new IceCandidate(
          jsonCandidate.get("candidate").getAsString(),
          jsonCandidate.get("sdpMid").getAsString(),
          jsonCandidate.get("sdpMLineIndex").getAsInt()
      );
      user.addCandidate(candidate);
    3. 随着客户端候选的收集,onIceCandidate消息会被发送很多次,后续的sdpMid可能是video,sdpMLineIndex可能是1

    4. 服务器端在创建端点后,也同样会自动收集ICE候选信息,并以iceCandidate消息发送给客户端。候选的内容如下:
      JavaScript
      1
      2
      3
      4
      5
      {
          "candidate": "candidate:5 1 TCP 1019216383 172.21.0.6 9 typ host tcptype active",
          "sdpMid":"video",
          "sdpMLineIndex":1
      }
    5. 客户端做如下处理:
      JavaScript
      1
      2
      3
      webRtcPeer.addIceCandidate(parsedMessage.candidate, function(error) {
          if (error)  console.error(error);
      });
    6. 随着服务器端候选的收集, iceCandidate消息也会被发送多次

  6. 随着候选信息的收集,webRtcPeer有了足够的信息,它会在remoteView元素中渲染远程媒体流
  7. 当用户点击停止按钮后,调用 webRtcPeer.dispose()并发送一个stop类型的消息
  8. 服务器收到stop消息后,清理用户数据:
    Java
    1
    2
    UserSession user = users.remove(session.getId());
    user.release();

    1. 释放用户数据的时候,会调用 mediaPipeline.release()释放媒体管线
  9. 页面卸载时,客户端自动关闭wss连接:
    JavaScript
    1
    ws.close();

在这个HelloWorld例子中,媒体流不是简单的由客户端发给自己,而是由服务器中转。也就是说,通信的Peer是服务器、客户端。

FaceOverlay

可以在上例的Loopback媒体管线上插入一个FaceOverlayFilter,在检测到人脸时,附加一个帽子图片到人脸上方:

Java
1
2
3
4
5
6
7
8
9
10
11
12
UserSession user = new UserSession();
MediaPipeline pipeline = kurento.createMediaPipeline();
WebRtcEndpoint webRtcEndpoint = new WebRtcEndpoint.Builder(pipeline).build();
user.setWebRtcEndpoint(webRtcEndpoint);
 
// 注意媒体管线在KMS中运行
FaceOverlayFilter faceOverlayFilter = new FaceOverlayFilter.Builder(pipeline).build();
faceOverlayFilter.setOverlayedImage(“https://172.21.0.1:8443/img/mario-wings.png", -0.35F, -1.2F, 1.6F, 1.6F);
// 连接WebRTC端点的SRC(输出)到FaceOverlayFilter的SINK(输入)
webRtcEndpoint.connect(faceOverlayFilter);
// 连接FaceOverlayFilter的SRC(输出)到WebRTC的SINK(输入)
faceOverlayFilter.connect(webRtcEndpoint);
一对多广播
发布者客户端

首先初始化连接:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
// 仅仅需要发送数据,不需要接收
webRtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(
    {
        localVideo : video,
        onicecandidate : function(){ /* 发送本地ICE候选信息给服务器 */ }
    },
    function( err ){
        webRtcPeer.generateOffer(function( err, offerSdp ){
            /* 发送SDP,消息类型presenter */
        });
    }
)

服务器接收到presenter消息后,会发送一个presenterResponse消息过来。如果服务器同意当前客户端作为发布者,则发布者调用:

JavaScript
1
2
webRtcPeer.processAnswer(message.sdpAnswer);
// 否则关闭连接

服务器发来的ICE候选消息的处理,和前面的例子一样。 

查看者客户端

首先也是初始化连接:

JavaScript
1
2
3
4
5
webRtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly( options, function( err ){
    this.generateOffer(function(){
        /* 发送SDP,消息类型viewer */
    });
});

viewerResponse、 服务器发来的ICE候选消息的处理,和发布者一样。

服务器

当服务器接收到发布者发来的presenter消息时,执行:

  1. 记录一个发布者的会话对象,本质上是基于WS客户端标识对发布者进行时别
  2. 创建媒体管线:
    Java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    pipeline = kurento.createMediaPipeline();
    // 设置发布者的端点对象
    presenterUserSession.setWebRtcEndpoint(new WebRtcEndpoint.Builder(pipeline).build());
    // 当服务器的ICE候选准备好之后,发送给发布者客户端:
    presenterUserSession.getWebRtcEndpoint().addIceCandidateFoundListener( e-> {
        // 作为iceCandidate事件发送
    });
     
    // 处理发布者的SDP
    String sdpOffer = jsonMessage.getAsJsonPrimitive("sdpOffer").getAsString();
    String sdpAnswer = presenterWebRtc.processOffer(sdpOffer);
    // 然后以presenterResponse消息发送SDP应答给发布者
     
    // 最后,为发布者收集ICE候选信息
    presenterWebRtc.gatherCandidates();
  3. 当接收到发布者的ICE候选后,把这些信息记录到代表发布者的会话对象中:
    Java
    1
    presenterUserSession.addCandidate(cand); // 处理方式和HelloWorld那个例子相同 

到目前为止,尚未发生任何媒体流的传输工作。因为没有人查看者。

当有查看者接入后,服务器首先收到一个viewer信息,并执行:

  1. 如果当前没有发布者,返回viewerResponse消息,其response属性为rejected
  2. 如果当前有发布者,则为其创建UserSession对象、WebRtcEndpoint端点,并发此端点加入到之前创建的管线中:
    Java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    UserSession viewer = new UserSession(webSocketSession);
    WebRtcEndpoint nextWebRtc = new WebRtcEndpoint.Builder(pipeline).build();
    viewer.setWebRtcEndpoint(nextWebRtc);
    viewer.getWebRtcEndpoint().addIceCandidateFoundListener( e-> {
        // 作为iceCandidate事件发送
    });
     
    // 重要:将发布者的SRC连接到查看者的SINK
    presenterUserSession.getWebRtcEndpoint().connect(nextWebRtc);
     
    // 处理查看者的SDP
    String sdpOffer = jsonMessage.getAsJsonPrimitive("sdpOffer").getAsString();
    // SDP应答总是调用请求者的端点对象获得
    String sdpAnswer = nextWebRtc.processOffer(sdpOffer);
    // 然后以viewerResponse消息发送SDP应答给发布者
     
    // 最后,为观看者收集ICE候选信息
    nextWebRtc.gatherCandidates();

当由更多的查看者连接进来后,发布者端点的SRC将连接到更多的SINK,呈现出星状结构。从ICE候选信息来看,貌似媒体流都是从服务器中转的。

一对一视频电话

这个在实现上没有特别的地方,参与通话双方的WebRTC端点,需要配置为首尾相连。

此外,业务逻辑部分需要实现拒接之类的功能。

多对多视频会议

相当于每个参与者都进行一对多广播。在实现时,往往会抽象出会议房间(Group)的概念,房间内的每个人都需要对其它人进行广播。

每个参与者都需要创建一个发送端点,N-1个接收端点,一共N个video元素。

此外,一旦有新人加入、旧人退出,就需要通知房间的所有参与者,进行客户端资源清理、UI更新。

视频监控

这类应用场景中,媒体流的来源主要有两类:

  1. 基于ONVIF框架协议,视频流基于RTSP/RTP传输
  2. 由设备SDK提供,SDK可能提供标准格式的码流、视频帧,或者解码后的原始图像

视频监控的主要需求包括:

  1. 实时监控,特别是多画面实时监控
  2. 录像回放
  3. 视频分析,例如移动侦测、模式识别
封装

为了简化开发,我们对Kurento、信号处理进行了组件化封装。

MediaSession

代表一个WebRTC客户端与Kurento的媒体会话:

Java
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
package cc.gmem.study.kurento;
 
 
import org.kurento.client.Endpoint;
import org.kurento.client.IceCandidate;
import org.kurento.client.MediaPipeline;
import org.kurento.client.WebRtcEndpoint;
 
import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
 
public class MediaSession {
 
    private String id;
 
    private Principal principal;
 
    private MediaPipeline pipeline;
 
    private WebRtcEndpoint endpoint;
 
    private List<IceCandidate> candidatesPending;
 
    public MediaSession( String id ) {
        this.id = id;
        candidatesPending = new ArrayList<>();
    }
 
    public MediaPipeline getPipeline() {
        return pipeline;
    }
 
    public void setPipeline( MediaPipeline pipeline ) {
        this.pipeline = pipeline;
    }
 
    public Endpoint getEndpoint() {
        return endpoint;
    }
 
    public synchronized void setEndpoint( WebRtcEndpoint endpoint ) {
        this.endpoint = endpoint;
        // ICE可能在端点创建之前就送达
        if ( candidatesPending != null ) {
            candidatesPending.forEach( cp -> {
                endpoint.addIceCandidate( cp );
            } );
            candidatesPending = null;
        }
    }
 
    @Override
    public String toString() {
        return String.format( "id = %s  ep = %s  pp = %s", getId(), getEndpoint(), getPipeline() );
    }
 
    public String getId() {
        return id;
    }
 
    public Principal getPrincipal() {
        return principal;
    }
 
    public void setPrincipal( Principal principal ) {
        this.principal = principal;
    }
 
    public synchronized void addIceCandidate( IceCandidate candidate ) {
        // ICE可能在端点创建之前就送达
        if ( endpoint == null ) {
            candidatesPending.add( candidate );
        } else {
            endpoint.addIceCandidate( candidate );
        }
    }
}
VideoSurveillanceApp

Spring Boot应用程序,信号处理以STOMP作为子协议:

Java
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
package cc.gmem.study.kurento;
 
import org.kurento.client.KurentoClient;
import org.kurento.client.KurentoClientBuilder;
import org.kurento.client.MediaPipeline;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptorAdapter;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import sun.security.acl.PrincipalImpl;
 
import java.security.Principal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
 
@SpringBootApplication
@EnableWebSocketMessageBroker
public class VideoSurveillanceApp extends AbstractWebSocketMessageBrokerConfigurer {
 
    private static final Logger LOGGER = LoggerFactory.getLogger( VideoSurveillanceApp.class );
 
    private Map<String, MediaSession> sessions = new ConcurrentHashMap<>();
 
    public void registerStompEndpoints( StompEndpointRegistry registry ) {
        // 信号处理在 /signal下进行
        registry.addEndpoint( "/signal" );
    }
 
    @Override
    public void configureMessageBroker( MessageBrokerRegistry registry ) {
        registry.setApplicationDestinationPrefixes( "/app" );
    }
 
    @Override
    public void configureClientInboundChannel( ChannelRegistration registration ) {
        registration.setInterceptors( new ChannelInterceptorAdapter() {
            @Override
            public Message<?> preSend( Message<?> message, MessageChannel channel ) {
                StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor( message, StompHeaderAccessor.class );
                String simpSessionId = (String) accessor.getHeader( "simpSessionId" );
                MediaSession session = getMideaSession( simpSessionId );
                if ( StompCommand.CONNECT.equals( accessor.getCommand() ) ) {
                    // 设置当前用户身份
                    String login = accessor.getNativeHeader( "login" ).get( 0 );
                    Principal principal = new PrincipalImpl( login );
                    accessor.setUser( principal );
                    session.setPrincipal( principal );
                    LOGGER.info( "User {} connected with session id {}", login, simpSessionId );
                }
                // 每次处理消息之前,设置session头,便于消息处理方法注入之
                accessor.setHeader( "session", session );
                return message;
            }
        } );
    }
 
    private MediaSession getMideaSession( String simpSessionId ) {
        if ( sessions.containsKey( simpSessionId ) ) {
            return sessions.get( simpSessionId );
        } else {
            MediaSession session = new MediaSession( simpSessionId );
            sessions.put( simpSessionId, session );
            return session;
        }
    }
 
    @Bean
    public KurentoClient kurentoClient() {
        return new KurentoClientBuilder().setKmsWsUri( "ws://172.21.0.6:8888/kurento" ).connect();
    }
 
    public static void main( String[] args ) {
        new SpringApplication( VideoSurveillanceApp.class ).run( args );
    }
}
KurentoService

封装一些模板代码:

Java
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
package cc.gmem.study.kurento;
 
import org.kurento.client.KurentoClient;
import org.kurento.client.MediaPipeline;
import org.kurento.client.WebRtcEndpoint;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
 
import javax.inject.Inject;
 
@Service
public class KurentoService {
 
    @Inject
    private KurentoClient client;
 
    @Inject
    private SimpMessagingTemplate template;
 
    /**
     * 初始化一个媒体管线
     *
     * @return
     */
    public MediaPipeline createMediaPipeline() {
        return client.createMediaPipeline();
    }
 
    /**
     * 在媒体管线上创建一个与WebRTC浏览器客户端通信的端点
     *
     * @param pipeline 管线
     * @param sdpoffer 浏览器发送来的SDP邀请
     * @param user     浏览器的身份
     * @return 运行在KMS中的WebRTC端点
     */
    public WebRtcEndpoint createWebRtcEndpoint( MediaPipeline pipeline, String sdpoffer, String user, String namespace ) {
        WebRtcEndpoint webRtcEndpoint = new WebRtcEndpoint.Builder( pipeline ).build();
        // 处理SDP
        String sdpAnswer = webRtcEndpoint.processOffer( sdpoffer );
        template.convertAndSendToUser( user, namespace + "/sdpanswer", sdpAnswer );
        // 处理ICE候选
        webRtcEndpoint.addIceCandidateFoundListener( event -> {
            String dest = namespace + "/icecandidate";
            template.convertAndSendToUser( user, dest, event.getCandidate() );
        } );
        webRtcEndpoint.gatherCandidates();
        return webRtcEndpoint;
    }
} 
StompClient 

对stomp.js进行简单的封装:

  1. 每个客户端在一个名字空间内操作
  2. 订阅总是针对/user前缀进行
  3. 发送总是针对/app前缀进行 

代码如下:

JavaScript
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
class StompClient {
    /**
     * 选项:
     * url,WebSocket连接地址
     * namespace,不包含/app、/user的目的地前缀
     * login,用户名
     * passcode,密码
     */
    constructor( options ) {
        this.namespace = options.namespace || {};
        this.pending = [];
        this.stomp = Stomp.over( new WebSocket( options.url ) );
        this.stomp.heartbeat.outgoing = 20000;
        this.stomp.connect( options.login, options.passcode, ( frame ) => {
            this.connected = true;
            this.processPending();
        } );
    }
 
    processPending() {
        if ( this.connected ) {
            let pending = this.pending;
            this.pending = [];
            pending.forEach( callback => callback() );
        }
    }
 
    recv( destination, callback ) {
        this.pending.push( () => {
            this.stomp.subscribe( '/user' + this.namespace + destination, ( frame ) => {
                callback( this.decode( frame.body, frame.headers[ 'content-type' ] ), frame );
            } );
        } );
        this.processPending();
    }
 
    encode( obj ) {
        return JSON.stringify( obj );
    }
 
    decode( str, mimeType ) {
        // 自动分析MIME类型,进行适当的解析
        if ( mimeType.startsWith( 'application/json;' ) ) {
            return JSON.parse( str );
        }
        else {
            return str;
        }
    }
 
    send( destination, object ) {
        this.pending.push( () => {
            this.stomp.send( '/app' + this.namespace + destination, {
                "content-type": "application/json;charset=UTF-8"
            }, this.encode( object ) );
        } );
        this.processPending();
    }
 
    disconnect() {
        this.stomp.disconnect();
    }
}
WebRTCEndpoint

对Kurento Utils的WebRtcPeer进行封装。 

WebRTCEndpoint的STOMP消息目的地格式:  前缀 + 名字空间 + 消息类型。消息类型包括:

  1. sdpoffer,表示浏览器客户端发起SDP邀请
  2. sdpanswer,表示KMS客户端发给浏览器的SDP应答
  3. icecandidate,双方交换ICE候选
  4. stop,客户端请求停止会话
  5. 其它消息类型

代码如下:

JavaScript
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
class WebRTCEndpoint {
    constructor( mode, options ) {
        /**
         * 选项:
         * remoteVideo,显示远程视频流的元素
         */
        options = options || {};
        let stomp = new StompClient( {
            url: options.url,
            namespace: options.namespace,
            login: options.login
        } );
 
        let webRtcPeerType;
        switch ( mode ) {
            case WebRTCEndpoint.MODE_SEND:
                webRtcPeerType = kurentoUtils.WebRtcPeer.WebRtcPeerSendonly;
                break;
            case WebRTCEndpoint.MODE_RECV:
                webRtcPeerType = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly;
                break;
            case WebRTCEndpoint.MODE_SEND_RECV:
                webRtcPeerType = kurentoUtils.WebRtcPeer.WebRtcPeerSendrecv;
                break;
        }
        stomp.recv( '/icecandidate', candidate => {
            this.peer.addIceCandidate( candidate );
        } );
        stomp.recv( '/sdpanswer', answer => {
            this.peer.processAnswer( answer );
        } );
        options.onicecandidate = candidate => {
            stomp.send( '/icecandidate', candidate );
        }
        this.peer = webRtcPeerType( options, err => {
            this.peer.generateOffer( ( error, sdpOffer ) => {
                stomp.send( '/sdpoffer', sdpOffer );
            } );
        } );
        this.stomp = stomp;
    }
 
    dispose() {
        this.stomp.send( '/stop', "bye" );
        this.stomp.disconnect();
        this.peer && this.peer.dispose();
    }
}
WebRTCEndpoint.MODE_SEND = 0;
WebRTCEndpoint.MODE_RECV = 1;
WebRTCEndpoint.MODE_SEND_RECV = 2; 
RTSP接入

以下情况下可以考虑RTSP接入:

  1. IP摄像头或者NVR直接提供流RTSP协议服务器
  2. 通过SDK获取码流,手工创建RTSP协议服务器

对于第二种方式,还可以考虑利用Kurento的RTPEndpint,直接通过RTP协议发送媒体流到KMS。

媒体互操作性

IP摄像头常常会提供某种基于流的接入方式:

  1. RTSP/H.264:这类摄像头通常用在视频监控领域。它们通过RTSP协议来建立RTP媒体会话 —— 信号处理基于RTSP进行而媒体流直接通过RTP传输。不同的摄像头厂商支持的RTP profile可能不同,AVP(用于音视频会议的RTP profile,最小化控制。RTP Profile for Audio and Video Conferences
    with Minimal Control)是一种常用的profile。视频编码方式也有不同的选择,典型的是 H.264
  2. HTTP/MJPEG:这类摄像头基于HTTP协议进行信号处理和媒体传输,视频流被编码为JPEG的序列。这类摄像头的硬件比较简单,资源(包括电量)消耗少但是视频质量差

要实现WebRTC到IP摄像头的媒体互操作性,两者的码流格式必须兼容,这种码流转换的工作是由某种WebRTC网关负责的(例如Kurento)。此网关需要完成:

  1. 和摄像头交互,也就是网关需要理解RTSP/RTP或者HTTP
  2. 解码从摄像头取得的码流,例如H264或者MJPEG
  3. 将码流重新编为浏览器支持的格式,例如VP8是WebRTC最广泛支持的编码
  4. 通过WebRTC协议把码流发送给客户端

此工作流示意如下图:

rtsp-gw

关于H.264

在Chrome中WebRTC使用的视频编码格式一直是VP8/VP9,直到Chrome 50才支持H264。你可以使用标记enable-webrtc-h264-with-openh264-ffmpeg打开H264支持(最新的Chrome 61此标记默认是打开的)。

H264被微软Edge的ORTC、Firefox、移动设备、遗留视频系统支持。移动设备大部分支持H264硬件解码,这意味着播放视频不会过于消耗电池,这一点很关键。

目前的情况并不乐观,主要是不同系统对于H.264的支持程度不同,它们可能支持不兼容的Profile,因而存在互操作性问题。

通信互操作性

WebRTC协议栈使用SAVPF这一RTP profile, 其含义是针对基于RTCP的反馈的扩展安全RTP profile(Extended Secure RTP Profile for Real-time Transport Control Protocol Based Feedback),SAVPF主要包括两个RTP profile:

  1. SAVP:AVP的基础上包含安全特性
  2. AVPF:用于及时的向媒体流的发送者反馈信息

SAVPF的意义在于,提供安全RTP通信的基础上支持反馈。WebRTC客户端会向WebRTC网关发送反馈信息(在RTCP包中),通知网关可能影响到媒体质量的网络状况。

大多数IP摄像头仅仅支持AVP,这意味着,网关无法把WebRTC的反馈传递给IP摄像头。网关必须自己管理好反馈信息,或者用行话说,网关必须终结(terminate)RTCP反馈。

这一点很重要,如果网关没有正确处理反馈,WebRTC客户端可能出现严重的QoS问题,通常是视频画面卡死。卡死的具体原因是:

  1. PLI(画面丢失提示,Picture Loss Indication)反馈:如果此反馈没有被网关正确处理,只要出现丢包,画面可能随机的卡死。这和VP8编码器的工作机制有关。VP8允许长时间没有关键帧生成(以分钟计),当PLI出现后网关应该立即生成新的关键帧,否则直到下一次关键帧(周期性的)到达,客户端都无法解码。某些网关的解决方式是,频繁的生成关键帧,这种做法的劣势是大量消耗带宽,导致视频质量差
  2. REMB(接收者估算的最大比特率, Receiver Estimated Maximum Bitrate)反馈:如果网关没有处理此反馈,且没有任何拥塞控制机制,则网关就不可能指示VP8编码器降低比特率。这样随着接入的客户端便多,网络带宽不够用后,视频质量变差
Kurento中接入RTSP

将Kurento作为WebRTC网关时,上述互操作性问题已经被解决,你需要了解以下三点:

  1. PlayerEndpoint这个端点支持从各种各样的源读取视频流,这些源可以是RTSP/RTP、HTTP/MJPEG。这意味着PlayerEndpoint有能力从IP摄像头读取码流
  2. WebRtcEndpoint这个端点支持完整的WebRTC协议栈,能够正确处理RTCP反馈:
    1. 每当PLI包被收到,WebRtcEndpoint会命令VP8编码器立即生成一个新的关键帧
    2. 内置了拥塞控制,且响应REMB包。必要时命令VP8编码器降低比特率
  3. 不可知媒体特性:当两个不兼容的媒体元素连接在一起时,Kurento会自动进行编码格式转换。也就是说H.264/MJPEG到VP8的转码会自动发生,不需要开发人员干预

RTSP到WebRTC的媒体管线示意如下:

rtsp-kurento

单画面接入代码

客户端代码:

rtsp-preview.html
XHTML
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
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WebRTC Video Surveillance - RTSP Preview</title>
    <script src="js/stomp.js"></script>
    <script src="js/stomp-wrapper.js"></script>
    <script src="js/adapter.js"></script>
    <script src="js/kurento-utils.js"></script>
    <script src="js/webrtc-endpoint.js"></script>
</head>
<body onunload="endpoint.dispose();">
<div>
    <video id="remoteVideo" autoplay width="427px" height="240px"></video>
</div>
<script>
    let endpoint = new WebRTCEndpoint( WebRTCEndpoint.MODE_RECV, {
        url: 'ws://172.21.0.1:9090/signal',
        namespace: '/rtsp/preview',
        login: new Date().getTime(),
        remoteVideo: document.getElementById( 'remoteVideo' )
    } );
</script>
</body>
</html>

服务器代码:

RtspPreviewController
Java
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
package cc.gmem.study.kurento;
 
import org.kurento.client.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
 
import javax.inject.Inject;
import java.security.Principal;
 
@Controller
@MessageMapping( RtspPreviewController.NAMESPACE )
public class RtspPreviewController {
 
    private static final Logger LOGGER = LoggerFactory.getLogger( RtspPreviewController.class );
 
    public static final String NAMESPACE = "/rtsp/preview";
 
    @Inject
    private KurentoService kurento;
 
    @MessageMapping( "/icecandidate" )
    public void onIceCandidate( IceCandidate candidate, @Header MediaSession session ) {
        WebRtcEndpoint endpoint = (WebRtcEndpoint) session.getEndpoint();
        session.addIceCandidate( candidate );
        return;
    }
 
    @MessageMapping( "/stop" )
    public void onStop( @Header MediaSession session ) {
        session.getEndpoint().release();
        session.getPipeline().release();
    }
 
    @MessageMapping( "/sdpoffer" )
    public void onSdpOffer( String sdpoffer, Principal principal, @Header MediaSession session ) {
 
        MediaPipeline pipeline = kurento.createMediaPipeline();
        session.setPipeline( pipeline );
 
        PlayerEndpoint.Builder peb = new PlayerEndpoint.Builder( pipeline, "rtsp://admin:12345@192.168.0.196:554/ch1/main/av_stream" );
        PlayerEndpoint playerEndpoint = peb.build();
        playerEndpoint.addMediaFlowInStateChangeListener( e -> {
            LOGGER.info( "RTSP input flow state changed, media type: {}, media state: {}", e.getMediaType(), e.getState() );
        } );
 
        WebRtcEndpoint webRtcEndpoint = kurento.createWebRtcEndpoint( pipeline, sdpoffer, principal.getName(), NAMESPACE );
        session.setEndpoint( webRtcEndpoint );
 
        playerEndpoint.connect( webRtcEndpoint );
        playerEndpoint.play();
    }
}
多画面接入代码

没有什么本质区别,只有一些技术上的细节需要处理:

rtsp-preview.html
XHTML
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
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WebRTC Video Surveillance - RTSP Preview</title>
    <script src="js/stomp.js"></script>
    <script src="js/stomp-wrapper.js"></script>
    <script src="js/adapter.js"></script>
    <script src="js/kurento-utils.js"></script>
    <script src="js/webrtc-endpoint.js"></script>
    <style>
        body {
            background-color: #02686c;
        }
 
        .nvbar {
            font-family: sans-serif;
            height: 30px;
            display: flex;
        }
 
        .title {
            font-size: 18px;
            margin-left: 12px;
            font-weight: bold;
            color: rgba(255, 255, 255, .8);
            text-shadow: 2px 2px 1px rgba(0, 0, 0, .8);
            line-height: 20px;
            vertical-align: text-bottom;
        }
 
        .subtitle {
            font-size: 14px;
            font-weight: bold;
            font-style: italic;
            margin-left: 24px;
            color: rgba(255, 255, 255, .5);
            line-height: 20px;
            vertical-align: text-bottom;
        }
 
        #videos {
            display: flex;
            flex-wrap: wrap;
        }
 
        #videos video {
            margin: 3px;
        }
 
        video {
            border-radius: 5px;
            border: 6px solid rgba(0, 0, 0, .5);
        }
 
        video:hover {
            border: 6px solid rgba(163, 163, 163, 0.5);
            cursor: pointer;
        }
    </style>
</head>
<body onunload="dispose();">
<div class="nvbar">
    <div class="title">基于WebRTC+Kurento的视频监控示例</div>
    <div class="subtitle">http://172.21.0.1:9090/rtsp-preview.html</div>
</div>
<div id="videos"></div>
 
<script>
    let endpoints = [];
    let videos = document.getElementById( 'videos' );
    for ( let ch = 1; ch <= 21; ch++ ) {
        let video = document.createElement( 'video' );
        video.autoplay = true;
        video.width = 352 / 2;
        video.height = 288 / 2;
        videos.appendChild( video );
        let endpoint = new WebRTCEndpoint( WebRTCEndpoint.MODE_RECV, {
            url: 'ws://172.21.0.1:9090/signal',
            namespace: '/rtsp/preview/ch1',
            // 因为执行速度太高,之前的new Date().getTime()两次调用会重复
            login: Math.random(),
            remoteVideo: video
        } );
        endpoints.push( endpoint );
 
    }
    function dispose() {
        endpoints.forEach( e => e.dispose() );
    }
</script>
</body>
</html>

服务器器端,每个视频通道使用一个名字空间(STOMP目的地中缀): 

Java
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
package cc.gmem.study.kurento;
 
import org.kurento.client.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.stereotype.Controller;
 
import javax.inject.Inject;
import java.security.Principal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
 
@Controller
@MessageMapping( RtspPreviewController.NSP )
public class RtspPreviewController {
 
    private static final Logger LOGGER = LoggerFactory.getLogger( RtspPreviewController.class );
 
    public static final String NSP = "/rtsp/preview";
 
    @Inject
    private KurentoService kurento;
 
    private Map<String, MediaPipeline> mediaPipelines = new ConcurrentHashMap<>();
 
    @MessageMapping( "/{ch}/icecandidate" )
    public void onIceCandidate( @DestinationVariable String ch, IceCandidate candidate, @Header MediaSession session ) {
        WebRtcEndpoint endpoint = (WebRtcEndpoint) session.getEndpoint();
        session.addIceCandidate( candidate );
        return;
    }
 
    @MessageMapping( "/{ch}/stop" )
    public void onStop( @DestinationVariable String ch, @Header MediaSession session ) {
        Endpoint endpoint = session.getEndpoint();
        // 获取连接到当前端点SINK的那些连接,注意,PlayerEndpoint会创建三个连接过来,分别用于AUDIO、VEDIO、DATA
        endpoint.getSourceConnections().forEach( data -> {
            MediaElement source = data.getSource();
            MediaElement sink = data.getSink();
            source.disconnect( sink );
        } );
        endpoint.release();
 
    }
 
    @MessageMapping( "/{ch}/sdpoffer" )
    public void onSdpOffer( @DestinationVariable String ch, String sdpoffer, Principal principal, @Header MediaSession session ) {
        // 媒体管线现在不是当前会话独占了,而是每个通道一个
        PlayerEndpoint playerEndpoint = getPlayerEndpoint( ch );
        MediaPipeline pipeline = playerEndpoint.getMediaPipeline();
        session.setPipeline( pipeline );
 
        WebRtcEndpoint webRtcEndpoint = kurento.createWebRtcEndpoint( pipeline, sdpoffer, principal.getName(), NSP + '/' + ch );
        session.setEndpoint( webRtcEndpoint );
 
        playerEndpoint.connect( webRtcEndpoint );
    }
 
    private synchronized PlayerEndpoint getPlayerEndpoint( String ch ) {
        MediaPipeline pipeline = getMediaPipline( ch );
        if ( pipeline == null ) {
            pipeline = kurento.createMediaPipeline();
            mediaPipelines.put( ch, pipeline );
            PlayerEndpoint.Builder peb = new PlayerEndpoint.Builder( pipeline, getRtspUrlFor( ch ) );
            PlayerEndpoint playerEndpoint = peb.build();
            playerEndpoint.addMediaFlowInStateChangeListener( e -> {
                LOGGER.info( "RTSP input flow state changed, media type: {}, media state: {}", e.getMediaType(), e.getState() );
            } );
            playerEndpoint.play();
            return playerEndpoint;
        } else {
            PlayerEndpoint playerEndpoint = null;
            for ( MediaObject mo : pipeline.getChildren() ) {
                if ( mo instanceof PlayerEndpoint ) {
                    playerEndpoint = (PlayerEndpoint) mo;
                }
            }
            return playerEndpoint;
        }
    }
 
    private MediaPipeline getMediaPipline( String ch ) {
        return mediaPipelines.get( ch );
    }
 
    /**
     * 获取指定通道的RTSP URL
     *
     * @param ch 通道号
     * @return
     */
    private String getRtspUrlFor( String ch ) {
        return "返回此通道的RTSP地址";
    }
} 

即使开到21画面,客户端运行仍然非常流畅( i7-4940MX ),完全可以满足视频监控领域的多画面需求:

rtsp-21scr

强制H264

前面我们已经提到过,Chrome 61默认已经开启了H264支持,其它很多浏览器也支持H264。如果KMS不进行转码,则对服务器配置要求可以大大降低。

首先,为Kurento安装插件:

Shell
1
apt install openh264-gst-plugins-bad-1.5

要强制KMS仅仅使用H264,可以修改KMS配置文件,注释掉VP8的支持:

/etc/kurento/modules/kurento/SdpEndpoint.conf.json
JavaScript
1
2
3
4
5
"videoCodecs" : [
    {
      "name" : "H264/90000"
    }
]

注意:

  1. 一定要确保你的客户端都支持基于H264的WebRTC视频传输,才可以进行上述修改
  2. 进行上述修改后,如果客户端不支持H264,那么SDP应答将会不完整,缺少媒体格式说明:
    1
    m=video 0 UDP/TLS/RTP/SAVPF    # 后面缺少媒体格式代码

    这会导致客户端WebRTC报错:

    1
    Failed to parse SessionDescription. m=video 0 UDP/TLS/RTP/SAVPF Expects at least 4 fields.
验证

通过SDP Offer/Answer可以查看浏览器和KMS协商使用H.264作为视频编码方式。

在浏览器地址栏输入 chrome://webrtc-internals,搜索ssrc_,会发现两个匹配项,其中一个和视频相关。可以看到mediaType为video,codecImplementationName为FFmpeg, googCodecName为H264。

问题

在公司的环境下测试,如果使用子码流的话,Chrome 61、Firefox都可以正常多画面播放。

但是采用主码流的情况下,运行效果实在太差:花屏、周期性卡死:

webrtc-chrome61-h264

打开chrome://webrtc-internals可以看到帧经常无法解析(framesDecoded计数不增加)。具体原因还需要深入研究,但我估计可能的相关因素有:

  1. 根据SDP,摄像头的H.264 Profile是420029,即Baseline;而Chrome支持的H.264 Profile是42e01f,即Constrained Baseline。也就是两者的Profile不兼容。这导致Kurento需要进行转码
  2. 如果进行SDP伪造,让Kurento相信Chrome支持420029,则完全无法播放。这意味着Chrome可能的确无法解码420029
  3. 转码工作依赖GST插件openh264-gst-plugins-bad-1.5完成,此插件可能存在质量问题

9月19日更新:

被摄像头给骗了……SDP声称的H.264 Profile和它实际使用的Profile并不一致。默认情况下这款摄像头使用的H.264 Profile为Main,手工配置之后则可以使用Baseline。

不过,就算改成Baseline,和Chrome/Firefox支持的Constrained Baseline仍然不兼容(注意:实际上很多编码器不使用Baseline特性针对Constrained Baseline的差集,也就是说两个Profile的编码结果很可能是兼容的)。进行SDP伪造的话,播放花屏、很快卡死。 

服务器端多画面合成

很多情况下,监控客户端都开启重要视频通道构成的固定多画面监控。这种情况下,可以考虑在流媒体服务器端把多画面合成GRID(例如四画面、九画面),好处是:

  1. 降低客户端解码压力
  2. 降低通信复杂度,不需要开启多个媒体连接甚至信号连接了

 

术语列表
WebRTC术语

更多WebRTC术语参考webrtcglossary。

术语 说明
ICE

交互式连接建立(Interactive Connectivity Establishment)是WebRTC进行NAT穿透的标准协议,由IETF RFC 5245定义。取决于candidate,ICE可能尝试直连、STUN、TURN —— ICE负责协调这三种底层连接机制

ICE通过指导连接性检测,来处理基于NATs的的媒体流连接。ICE收集所有可用的候选(candidate,可供Peer连接的地址信息):

  1. 对于STUN来说是本地IP地址、反射(reflexive)地址
  2. 对于TURN来说是中继地址

所有收集到的candidate通过SDP发送给Peer

一旦WebRTC收集流自己的、Peer的所有ICE地址之后,它就开始初始化连接性测试,逐个通过candidate发送媒体流直到成功

使用ICE的缺点是,会引入延迟(可能高达10s),新协议Trickle ICE用于解决此问题

ICE-TCP

通过TCP而不是TURN来发送媒体流的机制,Chrome支持

MCU

多点会议单元(Multipoint Conferencing Unit)

jsonMessage.get("candidate")这种设备提供了在单个视频/音频会话中,连接很多参与者的能力。MCU通常都实现了Mixing架构,因而每个会话都需要消耗很多计算资源

Mixing

一种多点通信架构,每个参与者发送自己的媒体流到中心服务器,并从中心服务器接收混合后的单个媒体流。实现此架构的服务器称为MCU。此架构的:

  1. 优势:对客户端要求低,客户端需要一个点对点连接
  2. 劣势:资源消耗高,因为服务器需要解码、布局、重新编码它接收到的媒体流
SDP

会话描述协议(Session Description Protocol),WebRTC使用该协议来协商会话的参数,但是WebRTC不负责信号处理,因而SDP的创建和传输需要应用程序自己完成

 SFU

选择性转发单元(Selective Forwarding Unit),有时用于描述一种视频路由设备,有时则用来描述一种路由特性

SFU能够接收多个媒体流,然后决定将其中的哪些流转发给哪些参与者

SIP

会话初始化协议(Session Initiation Protocol),一个在VoIP领域(电信行业)广泛使用的信号处理协议

STUN

NAT用会话穿透工具(Session Traversal Utilities for NAT) 是WebRTC进行NAT穿透的标准方法

STUN的核心目的是,探测客户端的公共地址/端口:

  1. 客户端发送STUN请求到服务器
  2. 服务器返回请求来自的公共地址信息
  3. 客户端通过SDP与Peer分析自己的公共地址信息
Trickle ICE

对 ICE的优化。ICE的主要瓶颈是初始化连接性检测比较耗时,Trickle ICE通过并行尝试多种底层机制,以加速candidate的获取。一旦某个candidate可用客户端就可以立即进行下一步,不需要等待所有candidate

TURN

基于中继的NAT穿透(Traversal Using Relays around NAT)是WebRTC进行NAT穿透的标准方法

当STUN不可用的情况下,TURN基于TURN服务器中继所有媒体流,这可能导致昂贵的流量和CPU开销

常见问题
如何循环播放

注册监听器,当流结束后,重新调用play():

Java
1
2
3
playerEndpoint.addEndOfStreamListener( e -> {
    playerEndpoint.play();
} );
← 使用Jansson处理JSON
Spring对WebSocket的支持 →
31 Comments On This Topic
  1. 回复
    juice
    2017/10/29

    总结的很仔细,适合初学者学习,感谢分享

  2. 回复
    juice
    2017/10/31

    你好。想请教一个问题,我这边也遇到了播放 rtsp 卡死的问题,查看 chrome 也是 framesDecoded 不增加,请问你这边怎么解决的,只能换摄像头吗?

    • 回复
      Alex
      2017/11/01

      是基于WebRTC的方案么,目前我们测试的结果是,Chrome下用VP8、VP9可以流畅播放,仅仅在使用H.264时出现卡死。

      卡死的具体原因没有深入研究,有可能是因为Chrome WebRTC对H.264支持不好,或者是Kurento使用的GStremer插件存在问题。

      • 回复
        juice
        2017/11/08

        你好,感谢你的回复
        我这边除了 Chrome 下测试之外,iOS 端也发现播放不了。
        所以现在尝试用 janus 代替,看是否正常,不过 janus 只支持 baseline profile 的编码

        • 回复
          Alex
          2017/11/16

          janus能够不转码和Web客户端配合进行H.264 over WebRTC么?

      • 回复
        juice
        2017/11/08

        我这边用 VP8 也是正常的,只是当摄像头一多起来,CPU 会飙升到 100%,估计是 kurento 将 h264 编码成 VP8 的原因
        所以才研究 chrome 和 iOS 直接播放 h264 的方法

        • 回复
          Alex
          2017/11/16

          嗯,路数多了重编码压力的确大,但是帧率、码率、分辨率都很大程度上影响CPU资源需求。像视频监控领域,多画面的时候都是用低质量的码流的,否则客户端也受不了。

          移动设备对H.264的支持应该都很好,不必基于Web技术开发,编码格式不是问题。

          Chrome这样的,我目前尝试倒是觉得JS软解码H.264是较为理想的途径,不过找到的解码库也仅支持Baseline。

  3. 回复
    Manpo
    2017/11/29

    如何循环播放
    注册监听器,当流结束后,重新调用play():
    playerEndpoint.addEndOfStreamListener( e -> {
    playerEndpoint.play();
    } );
    如果是file:// 文件则不行。

    另外,如何配置kurento 采用VP9编码呢?现在chrome VP9性能更优。

    • 回复
      Alex
      2017/11/29

      Kurento本身不支持VP9,而且此项目不怎么更新,所以需要更高的可定制性,可以从Kurento的底层GStreamer着手,或选用其它WebRTC服务器

      • 回复
        Manpo
        2017/11/30

        感谢回复。

  4. 回复
    suzhq
    2018/01/23

    写的真不错,这方面的中文实践资料真不多,非常感谢。
    我想请问一下,各家的 webrtc之间是不是也不兼容呀,这个Kurento是支持那些webrtc呢?
    这个问题比较小白,见笑了。

    • 回复
      Alex
      2018/01/23

      WebRTC兼容性表格请参考:http://iswebrtcreadyyet.com/legacy.html

      各家浏览器对音视频编码方式的支持有所差异

      Kurento对VP8的支持较好,在服务器端可以自适应的转换为VP8码流。我们之前测试Kurento对H264的支持不是很理想。

  5. 回复
    zjyishi
    2018/06/06

    请教一下,在源代码中怎么找不到一些类的定义呢?例如MediaObject、MediaElement、ServerType、ServerInfo等等,有什么玄机吗?非常感谢!

    • 回复
      Alex
      2018/06/06

      你好,这些属于API,Java的代码在 https://github.com/Kurento/kurento-java 项目中

      • 回复
        zjyishi
        2018/06/07

        非常感谢!
        我今天尝试按照你上面的步骤下载了源码,希望能通过编译源码搭建起kms的环境,目前全部都已经编译通过了,但是在采用上面提到的脚本运行的时候,出现了以下错误:
        0:00:00.048094948 6481 0x55af2e706300 ERROR KurentoLoadConfig loadConfig.cpp:237:loadConfig: Error reading configuration: /home/liush/cpp/lib/kurento/etc/kurento/kurento.conf.json(4): expected key string
        Error reading configuration: /home/liush/cpp/lib/kurento/etc/kurento/kurento.conf.json(4): expected key string
        不知道你这边是否有遇到过,谢谢!

  6. 回复
    zjyishi
    2018/06/09

    再次感谢Alex的文章,自己编译源码的环境终于跑起来了!
    总结一下自己的问题:
    1、MediaObject等类的头文件是在编译kurento-module-creator的时候自动生成;
    2、运行时出现读取配置文件异常,是由于现在的最新版本6.7.1读取json配置文件,不能有注释行(我把配置文件中的注释行全删除后就能正常运行了),通过apt-get安装的kms-6则就可以有注释行,真奇怪!

  7. 回复
    rain
    2018/11/30

    是不是说同样是h264,因为profile不一样,也会触发服务器端转码?

    • 回复
      Alex
      2018/11/30

      因为浏览器支持的是H264的子集,如果不转码它没法播放。

  8. 回复
    rain
    2018/11/30

    我搜资料看,好像如果直接播放mp4就没有profile的问题,但webrtc会有profile问题

    • 回复
      Alex
      2018/12/04

      恩,我们这里谈到的编码方式限制,都是针对基于WebRTC的“实时播放”场景的。

  9. 回复
    rain
    2018/12/05

    kurento是否可以不使用STUN'和TUREN,比如监控只在内网使用 ,也不能连接外网,是不是有办法可以免去安装coturn?

    • 回复
      Alex
      2018/12/06

      不需要启用,我们的应用场景就是局域网,没有使用过这些特性。

  10. 回复
    alanthinker
    2018/12/13

    请教一个问题, 网络摄像头一般只支持2, 3 个用户同时读取rtsp流, 能否让服务器只读取一个rtps流, 但支持多个用户播放这个转码后的流?

    • 回复
      Alex
      2018/12/13

      当然可以,这就是流媒体服务器的功能。它负责接入一台前端设备(摄像头),然后将媒体流分发给多个用户

  11. 回复
    rain
    2018/12/21

    最新版 6.9.0 跑不起来rtsp源了

    • 回复
      rain
      2018/12/25

      这个问题6.9.1解决了

  12. 回复
    rain
    2018/12/25

    转码过程中能设置分辨率和帧率吗?比如rtsp源是1080p的转码后编程720p的。

  13. 回复
    kris
    2020/02/20

    你好,我想问一下客户端由于网络问题进行断开连接,有没有什么处理办法

  14. 回复
    zhuyan
    2020/03/19

    接入摄像头多,如何降低画面质量?

  15. 回复
    许铭毅
    2020/08/07

    您再次博客中贴出的RTSP接入部分的代码可以发下吗?

  16. 回复
    许铭毅
    2020/08/07

    您这次博客中贴出的RTSP接入部分的代码可以发下吗?

Leave a Reply to Alex Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

Related Posts

  • HTML5视频监控技术预研
  • 基于Broadway的HTML5视频监控
  • 基于MinGW的海康视频监控开发
  • 实时通信协议族
  • OpenAPI学习笔记

Recent Posts

  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
  • A Comprehensive Study of Kotlin for Java Developers
  • 背诵营笔记
  • 利用LangChain和语言模型交互
  • 享学营笔记
ABOUT ME

汪震 | Alex Wong

江苏淮安人,现居北京。目前供职于腾讯云,专注容器方向。

GitHub:gmemcc

Git:git.gmem.cc

Email:gmemjunk@gmem.cc@me.com

ABOUT GMEM

绿色记忆是我的个人网站,域名gmem.cc中G是Green的简写,MEM是Memory的简写,CC则是我的小天使彩彩名字的简写。

我在这里记录自己的工作与生活,同时和大家分享一些编程方面的知识。

GMEM HISTORY
v2.00:微风
v1.03:单车旅行
v1.02:夏日版
v1.01:未完成
v0.10:彩虹天堂
v0.01:阳光海岸
MIRROR INFO
Meta
  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
Recent Posts
  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
    In this blog post, I will walk ...
  • A Comprehensive Study of Kotlin for Java Developers
    Introduction Purpose of the Study Understanding the Mo ...
  • 背诵营笔记
    Day 1 Find Your Greatness 原文 Greatness. It’s just ...
  • 利用LangChain和语言模型交互
    LangChain是什么 从名字上可以看出来,LangChain可以用来构建自然语言处理能力的链条。它是一个库 ...
  • 享学营笔记
    Unit 1 At home Lesson 1 In the ...
  • K8S集群跨云迁移
    要将K8S集群从一个云服务商迁移到另外一个,需要解决以下问题: 各种K8S资源的迁移 工作负载所挂载的数 ...
  • Terraform快速参考
    简介 Terraform用于实现基础设施即代码(infrastructure as code)—— 通过代码( ...
  • 草缸2021
    经过四个多月的努力,我的小小荷兰景到达极致了状态。

  • 编写Kubernetes风格的APIServer
    背景 前段时间接到一个需求做一个工具,工具将在K8S中运行。需求很适合用控制器模式实现,很自然的就基于kube ...
  • 记录一次KeyDB缓慢的定位过程
    环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点: ...
  • eBPF学习笔记
    简介 BPF,即Berkeley Packet Filter,是一个古老的网络封包过滤机制。它允许从用户空间注 ...
  • IPVS模式下ClusterIP泄露宿主机端口的问题
    问题 在一个启用了IPVS模式kube-proxy的K8S集群中,运行着一个Docker Registry服务 ...
  • 念爷爷
      今天是爷爷的头七,十二月七日、阴历十月廿三中午,老人家与世长辞。   九月初,回家看望刚动完手术的爸爸,发

  • 6 杨梅坑

  • liuhuashan
    深圳人才公园的网红景点 —— 流花山

  • 1 2020年10月拈花湾

  • 内核缺陷触发的NodePort服务63秒延迟问题
    现象 我们有一个新创建的TKE 1.3.0集群,使用基于Galaxy + Flannel(VXLAN模式)的容 ...
  • Galaxy学习笔记
    简介 Galaxy是TKEStack的一个网络组件,支持为TKE集群提供Overlay/Underlay容器网 ...
TOPLINKS
  • Zitahli's blue 91 people like this
  • 梦中的婚礼 64 people like this
  • 汪静好 61 people like this
  • 那年我一岁 36 people like this
  • 为了爱 28 people like this
  • 小绿彩 26 people like this
  • 彩虹姐姐的笑脸 24 people like this
  • 杨梅坑 6 people like this
  • 亚龙湾之旅 1 people like this
  • 汪昌博 people like this
  • 2013年11月香山 10 people like this
  • 2013年7月秦皇岛 6 people like this
  • 2013年6月蓟县盘山 5 people like this
  • 2013年2月梅花山 2 people like this
  • 2013年淮阴自贡迎春灯会 3 people like this
  • 2012年镇江金山游 1 people like this
  • 2012年徽杭古道 9 people like this
  • 2011年清明节后扬州行 1 people like this
  • 2008年十一云龙公园 5 people like this
  • 2008年之秋忆 7 people like this
  • 老照片 13 people like this
  • 火一样的六月 16 people like this
  • 发黄的相片 3 people like this
  • Cesium学习笔记 90 people like this
  • IntelliJ IDEA知识集锦 59 people like this
  • Bazel学习笔记 38 people like this
  • 基于Kurento搭建WebRTC服务器 38 people like this
  • PhoneGap学习笔记 32 people like this
  • NaCl学习笔记 32 people like this
  • 使用Oracle Java Mission Control监控JVM运行状态 29 people like this
  • Ceph学习笔记 27 people like this
  • 基于Calico的CNI 27 people like this
Tag Cloud
ActiveMQ AspectJ CDT Ceph Chrome CNI Command Cordova Coroutine CXF Cygwin DNS Docker eBPF Eclipse ExtJS F7 FAQ Groovy Hibernate HTTP IntelliJ IO编程 IPVS JacksonJSON JMS JSON JVM K8S kernel LB libvirt Linux知识 Linux编程 LOG Maven MinGW Mock Monitoring Multimedia MVC MySQL netfs Netty Nginx NIO Node.js NoSQL Oracle PDT PHP Redis RPC Scheduler ServiceMesh SNMP Spring SSL svn Tomcat TSDB Ubuntu WebGL WebRTC WebService WebSocket wxWidgets XDebug XML XPath XRM ZooKeeper 亚龙湾 单元测试 学习笔记 实时处理 并发编程 彩姐 性能剖析 性能调优 文本处理 新特性 架构模式 系统编程 网络编程 视频监控 设计模式 远程调试 配置文件 齐塔莉
Recent Comments
  • qg on Istio中的透明代理问题
  • heao on 基于本地gRPC的Go插件系统
  • 黄豆豆 on Ginkgo学习笔记
  • cloud on OpenStack学习笔记
  • 5dragoncon on Cilium学习笔记
  • Archeb on 重温iptables
  • C/C++编程:WebSocketpp(Linux + Clion + boostAsio) – 源码巴士 on 基于C/C++的WebSocket库
  • jerbin on eBPF学习笔记
  • point on Istio中的透明代理问题
  • G on Istio中的透明代理问题
  • 绿色记忆:Go语言单元测试和仿冒 on Ginkgo学习笔记
  • point on Istio中的透明代理问题
  • 【Maven】maven插件开发实战 – IT汇 on Maven插件开发
  • chenlx on eBPF学习笔记
  • Alex on eBPF学习笔记
  • CFC4N on eBPF学习笔记
  • 李运田 on 念爷爷
  • yongman on 记录一次KeyDB缓慢的定位过程
  • Alex on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • haolipeng on 基于本地gRPC的Go插件系统
  • 吴杰 on 基于C/C++的WebSocket库
©2005-2025 Gmem.cc | Powered by WordPress | 京ICP备18007345号-2