diff --git a/im-ui/src/api/camera.js b/im-ui/src/api/camera.js
new file mode 100644
index 0000000..be3a1ae
--- /dev/null
+++ b/im-ui/src/api/camera.js
@@ -0,0 +1,76 @@
+
+class ImCamera {
+ constructor() {
+ this.stream = null;
+ }
+}
+
+ImCamera.prototype.isEnable = function() {
+ return !!navigator && !!navigator.mediaDevices && !!navigator.mediaDevices.getUserMedia;
+}
+
+ImCamera.prototype.openVideo = function(isFacing) {
+ return new Promise((resolve, reject) => {
+ if(this.stream){
+ this.close()
+ }
+ let facingMode = isFacing ? "user" : "environment";
+ let constraints = {
+ video: {
+ facingMode: facingMode
+ },
+ audio: {
+ echoCancellation: true, //音频开启回音消除
+ noiseSuppression: true // 开启降噪
+ }
+ }
+ console.log("getUserMedia")
+ navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
+ console.log("摄像头打开")
+ this.stream = stream;
+ resolve(stream);
+ }).catch((e) => {
+ console.log(e)
+ console.log("摄像头未能正常打开")
+ reject({
+ code: 0,
+ message: "摄像头未能正常打开"
+ })
+ })
+ })
+}
+
+
+ImCamera.prototype.openAudio = function() {
+ return new Promise((resolve, reject) => {
+ let constraints = {
+ video: false,
+ audio: {
+ echoCancellation: true, //音频开启回音消除
+ noiseSuppression: true // 开启降噪
+ }
+ }
+ navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
+ this.stream = stream;
+ resolve(stream);
+ }).catch(() => {
+ console.log("麦克风未能正常打开")
+ reject({
+ code: 0,
+ message: "麦克风未能正常打开"
+ })
+ })
+
+ })
+}
+
+ImCamera.prototype.close = function() {
+ // 停止流
+ if (this.stream) {
+ this.stream.getTracks().forEach((track) => {
+ track.stop();
+ });
+ }
+}
+
+export default ImCamera;
\ No newline at end of file
diff --git a/im-ui/src/api/eventBus.js b/im-ui/src/api/eventBus.js
new file mode 100644
index 0000000..a72b416
--- /dev/null
+++ b/im-ui/src/api/eventBus.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
\ No newline at end of file
diff --git a/im-ui/src/api/rtcGroupApi.js b/im-ui/src/api/rtcGroupApi.js
new file mode 100644
index 0000000..58e8409
--- /dev/null
+++ b/im-ui/src/api/rtcGroupApi.js
@@ -0,0 +1,156 @@
+import http from './httpRequest.js'
+
+class RtcGroupApi {
+ constructor() {
+ this.http = http;
+ }
+}
+
+
+RtcGroupApi.prototype.setup = function(groupId, userInfos) {
+ let formData = {
+ groupId,
+ userInfos
+ }
+ return this.http({
+ url: '/webrtc/group/setup',
+ method: 'post',
+ data: formData
+ })
+}
+
+RtcGroupApi.prototype.accept = function(groupId) {
+ return this.http({
+ url: '/webrtc/group/accept?groupId='+groupId,
+ method: 'post'
+ })
+}
+
+RtcGroupApi.prototype.reject = function(groupId) {
+ return this.http({
+ url: '/webrtc/group/reject?groupId='+groupId,
+ method: 'post'
+ })
+}
+
+RtcGroupApi.prototype.failed = function(groupId,reason) {
+ let formData = {
+ groupId,
+ reason
+ }
+ return this.http({
+ url: '/webrtc/group/failed',
+ method: 'post',
+ data: formData
+ })
+}
+
+
+RtcGroupApi.prototype.join = function(groupId) {
+ return this.http({
+ url: '/webrtc/group/join?groupId='+groupId,
+ method: 'post'
+ })
+}
+
+RtcGroupApi.prototype.invite = function(groupId, userInfos) {
+ let formData = {
+ groupId,
+ userInfos
+ }
+ return this.http({
+ url: '/webrtc/group/invite',
+ method: 'post',
+ data: formData
+ })
+}
+
+
+RtcGroupApi.prototype.offer = function(groupId, userId, offer) {
+ let formData = {
+ groupId,
+ userId,
+ offer
+ }
+ return this.http({
+ url: '/webrtc/group/offer',
+ method: 'post',
+ data: formData
+ })
+}
+
+RtcGroupApi.prototype.answer = function(groupId, userId, answer) {
+ let formData = {
+ groupId,
+ userId,
+ answer
+ }
+ return this.http({
+ url: '/webrtc/group/answer',
+ method: 'post',
+ data: formData
+ })
+}
+
+RtcGroupApi.prototype.quit = function(groupId) {
+ return this.http({
+ url: '/webrtc/group/quit?groupId=' + groupId,
+ method: 'post'
+ })
+}
+
+RtcGroupApi.prototype.cancel = function(groupId) {
+ return this.http({
+ url: '/webrtc/group/cancel?groupId=' + groupId,
+ method: 'post'
+ })
+}
+
+RtcGroupApi.prototype.candidate = function(groupId, userId, candidate) {
+ let formData = {
+ groupId,
+ userId,
+ candidate
+ }
+ return this.http({
+ url: '/webrtc/group/candidate',
+ method: 'post',
+ data: formData
+ })
+}
+
+RtcGroupApi.prototype.device = function(groupId, isCamera, isMicroPhone) {
+ let formData = {
+ groupId,
+ isCamera,
+ isMicroPhone
+ }
+ return this.http({
+ url: '/webrtc/group/device',
+ method: 'post',
+ data: formData
+ })
+}
+
+
+RtcGroupApi.prototype.candidate = function(groupId, userId, candidate) {
+ let formData = {
+ groupId,
+ userId,
+ candidate
+ }
+ return this.http({
+ url: '/webrtc/group/candidate',
+ method: 'post',
+ data: formData
+ })
+}
+
+RtcGroupApi.prototype.heartbeat = function(groupId) {
+ return this.http({
+ url: '/webrtc/group/heartbeat?groupId=' + groupId,
+ method: 'post'
+ })
+}
+
+export default RtcGroupApi;
\ No newline at end of file
diff --git a/im-ui/src/api/webrtc.js b/im-ui/src/api/webrtc.js
new file mode 100644
index 0000000..359118b
--- /dev/null
+++ b/im-ui/src/api/webrtc.js
@@ -0,0 +1,117 @@
+
+class ImWebRtc {
+ constructor() {
+ this.configuration = {}
+ this.stream = null;
+ }
+}
+
+ImWebRtc.prototype.isEnable = function() {
+ window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window
+ .mozRTCPeerConnection;
+ window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window
+ .mozRTCSessionDescription;
+ window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window
+ .mozRTCIceCandidate;
+ return !!window.RTCPeerConnection;
+}
+
+ImWebRtc.prototype.init = function(configuration) {
+ this.configuration = configuration;
+}
+
+ImWebRtc.prototype.setupPeerConnection = function(callback) {
+ this.peerConnection = new RTCPeerConnection(this.configuration);
+ this.peerConnection.ontrack = (e) => {
+ // 对方的视频流
+ callback(e.streams[0]);
+ };
+}
+
+
+ImWebRtc.prototype.setStream = function(stream) {
+ if(this.stream){
+ this.peerConnection.removeStream(this.stream)
+ }
+ stream.getTracks().forEach((track) => {
+ this.peerConnection.addTrack(track, stream);
+ });
+ this.stream = stream;
+}
+
+
+ImWebRtc.prototype.onIcecandidate = function(callback) {
+ this.peerConnection.onicecandidate = (event) => {
+ // 追踪到候选信息
+ if (event.candidate) {
+ callback(event.candidate)
+ }
+ }
+}
+
+ImWebRtc.prototype.onStateChange = function(callback) {
+ // 监听连接状态
+ this.peerConnection.oniceconnectionstatechange = (event) => {
+ let state = event.target.iceConnectionState;
+ console.log("ICE连接状态变化: : " + state)
+ callback(state)
+ };
+}
+
+ImWebRtc.prototype.createOffer = function() {
+ return new Promise((resolve, reject) => {
+ const offerParam = {};
+ offerParam.offerToRecieveAudio = 1;
+ offerParam.offerToRecieveVideo = 1;
+ // 创建本地sdp信息
+ this.peerConnection.createOffer(offerParam).then((offer) => {
+ // 设置本地sdp信息
+ this.peerConnection.setLocalDescription(offer);
+ // 发起呼叫请求
+ resolve(offer)
+ }).catch((e) => {
+ reject(e)
+ })
+ });
+}
+
+
+ImWebRtc.prototype.createAnswer = function(offer) {
+ return new Promise((resolve, reject) => {
+ // 设置远端的sdp
+ this.setRemoteDescription(offer);
+ // 创建本地dsp
+ const offerParam = {};
+ offerParam.offerToRecieveAudio = 1;
+ offerParam.offerToRecieveVideo = 1;
+ this.peerConnection.createAnswer(offerParam).then((answer) => {
+ // 设置本地sdp信息
+ this.peerConnection.setLocalDescription(answer);
+ // 接受呼叫请求
+ resolve(answer)
+ }).catch((e) => {
+ reject(e)
+ })
+ });
+}
+
+ImWebRtc.prototype.setRemoteDescription = function(offer) {
+ // 设置对方的sdp信息
+ this.peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
+}
+
+ImWebRtc.prototype.addIceCandidate = function(candidate) {
+ // 添加对方的候选人信息
+ this.peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
+}
+
+ImWebRtc.prototype.close = function(uid) {
+ // 关闭RTC连接
+ if (this.peerConnection) {
+ this.peerConnection.close();
+ this.peerConnection.onicecandidate = null;
+ this.peerConnection.onaddstream = null;
+ }
+}
+
+export default ImWebRtc;
\ No newline at end of file
diff --git a/im-ui/src/components/rtc/RtcGroupJoin.vue b/im-ui/src/components/rtc/RtcGroupJoin.vue
new file mode 100644
index 0000000..7d09b10
--- /dev/null
+++ b/im-ui/src/components/rtc/RtcGroupJoin.vue
@@ -0,0 +1,116 @@
+
+