java+netty+webrtc语音视频屏幕共享聊天室设计实践(代码片段)

殷长庆 殷长庆     2023-04-06     759

关键词:

背景

本文使用webtrc实现了一个简单的语音视频聊天室、支持多人音视频聊天、屏幕共享。

环境配置

音视频功能需要在有Https协议的域名下才能获取到设备信息,

测试环境搭建Https服务参考Windows下Nginx配置SSL实现Https访问(包含openssl证书生成)_殷长庆的博客-CSDN博客

正式环境可以申请一个免费的证书 

复杂网络环境下需要自己搭建turnserver,网络上搜索大多是使用coturn来搭建turn服务 

turn默认监听端口3478,可以使用webrtc.github.io 测试服务是否可用

本文在局域网内测试,不必要部署turn,使用的谷歌的stun:stun.l.google.com:19302

webrtc参考文章

WebRTC技术简介 - 知乎 (zhihu.com)

实现 

服务端 

服务端使用netty构建一个websocket服务,用来完成为音视频传递ICE信息等工作。 

maven配置

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.luck.cc</groupId>
	<artifactId>cc-im</artifactId>
	<version>1.0-SNAPSHOT</version>
	<name>cc-im</name>
	<url>http://maven.apache.org</url>

	<properties>
		<java.home>$env.JAVA_HOME</java.home>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>io.netty</groupId>
			<artifactId>netty-all</artifactId>
			<version>4.1.74.Final</version>
		</dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.5.7</version>
        </dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
				</configuration>
			</plugin>
	        <plugin>
	            <artifactId>maven-assembly-plugin</artifactId>
	            <version>3.0.0</version>
	            <configuration>
	                <archive>
	                    <manifest>
	                        <mainClass>com.luck.im.ServerStart</mainClass>
	                    </manifest>
	                </archive>
	                <descriptorRefs>
	                    <descriptorRef>jar-with-dependencies</descriptorRef>
	                </descriptorRefs>
	            </configuration>
	            <executions>
	                <execution>
	                    <id>make-assembly</id>
	                    <phase>package</phase>
	                    <goals>
	                        <goal>single</goal>
	                    </goals>
	                </execution>
	            </executions>
	        </plugin>
		</plugins>
	</build>
</project>

 JAVA代码

 聊天室服务

package com.luck.im;

import java.util.List;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.MessageToMessageCodec;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;

public class ChatSocket 
	private static EventLoopGroup bossGroup = new NioEventLoopGroup();
	private static EventLoopGroup workerGroup = new NioEventLoopGroup();
	private static ChannelFuture channelFuture;

	/**
	 * 启动服务代理
	 * 
	 * @throws Exception
	 */
	public static void startServer() throws Exception 
		try 
			ServerBootstrap b = new ServerBootstrap();
			b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
					.childHandler(new ChannelInitializer<SocketChannel>() 
						@Override
						public void initChannel(SocketChannel ch) throws Exception 
							ChannelPipeline pipeline = ch.pipeline();
							pipeline.addLast(new HttpServerCodec());
							pipeline.addLast(
									new WebSocketServerProtocolHandler("/myim", null, true, Integer.MAX_VALUE, false));
							pipeline.addLast(new MessageToMessageCodec<TextWebSocketFrame, String>() 
								@Override
								protected void decode(ChannelHandlerContext ctx, TextWebSocketFrame frame,
										List<Object> list) throws Exception 
									list.add(frame.text());
								

								@Override
								protected void encode(ChannelHandlerContext ctx, String msg, List<Object> list)
										throws Exception 
									list.add(new TextWebSocketFrame(msg));
								
							);
							pipeline.addLast(new ChatHandler());
						
					);
			channelFuture = b.bind(8321).sync();

			channelFuture.channel().closeFuture().sync();
		 finally 
			shutdown();
			// 服务器已关闭
		
	

	public static void shutdown() 
		if (channelFuture != null) 
			channelFuture.channel().close().syncUninterruptibly();
		
		if ((bossGroup != null) && (!bossGroup.isShutdown())) 
			bossGroup.shutdownGracefully();
		
		if ((workerGroup != null) && (!workerGroup.isShutdown())) 
			workerGroup.shutdownGracefully();
		
	

聊天室业务 

package com.luck.im;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.AttributeKey;
import io.netty.util.internal.StringUtil;

public class ChatHandler extends SimpleChannelInboundHandler<String> 

	/** 用户集合 */
	private static Map<String, Channel> umap = new ConcurrentHashMap<>();

	/** channel绑定自己的用户ID */
	public static final AttributeKey<String> UID = AttributeKey.newInstance("uid");

	@Override
	public void channelRead0(ChannelHandlerContext ctx, String msg) 
		JSONObject parseObj = JSONUtil.parseObj(msg);
		Integer type = parseObj.getInt("t");
		String uid = parseObj.getStr("uid");
		String tid = parseObj.getStr("tid");
		switch (type) 
		case 0:
			// 心跳
			break;
		case 1:
			// 用户加入聊天室
			umap.put(uid, ctx.channel());
			ctx.channel().attr(UID).set(uid);
			umap.forEach((x, y) -> 
				if (!x.equals(uid)) 
					JSONObject json = new JSONObject();
					json.set("t", 2);
					json.set("uid", uid);
					json.set("type", "join");
					y.writeAndFlush(json.toString());
				
			);
			break;
		case 2:
			Channel uc = umap.get(tid);
			if (null != uc) 
				uc.writeAndFlush(msg);
			
			break;
		case 9:
			// 用户退出聊天室
			umap.remove(uid);
			leave(ctx, uid);
			ctx.close();
			break;
		default:
			break;
		
	

	@Override
	public void channelInactive(ChannelHandlerContext ctx) throws Exception 
		String uid = ctx.channel().attr(UID).get();
		if (StringUtil.isNullOrEmpty(uid)) 
			super.channelInactive(ctx);
			return;
		
		ctx.channel().attr(UID).set(null);
		umap.remove(uid);
		leave(ctx, uid);
		super.channelInactive(ctx);
	

	/**
	 * 用户退出
	 * 
	 * @param ctx
	 * @param uid
	 */
	private void leave(ChannelHandlerContext ctx, String uid) 
		umap.forEach((x, y) -> 
			if (!x.equals(uid)) 
				// 把数据转到用户服务
				JSONObject json = new JSONObject();
				json.set("t", 9);
				json.set("uid", uid);
				y.writeAndFlush(json.toString());
			
		);
	

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception 
		cause.printStackTrace();
		ctx.close();
	

启动类

package com.luck.im;

public class ServerStart 
	public static void main(String[] args) throws Exception 
		// 启动聊天室
		ChatSocket.startServer();
	

前端

网页主要使用了adapter-latest.js,下载地址webrtc.github.io

github访问不了可以用webrtc/adapter-latest.js-Javascript文档类资源-CSDN文库 

index.html 

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>聊天室</title>
	<style>videowidth:100px;height:100px</style>
</head>
<body>
<video id="localVideo" autoplay playsinline></video>
<video id="screenVideo" autoplay playsinline></video>
<div id="videos"></div>
<div id="screenVideos"></div>
<div>
<button onclick="startScreen()">开启屏幕共享</button>
<button onclick="closeScreen()">关闭屏幕共享</button>
<button onclick="startVideo()">开启视频</button>
<button onclick="closeVideo()">关闭视频</button>
<button onclick="startAudio()">开启语音</button>
<button onclick="closeAudio()">关闭语音</button>
<button onclick="leave()">退出</button>
</div>
</body>
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script>
function getUid(id)
	return id?id:uid;

// 开启屏幕共享
function startScreen(id)
	id=getUid(id);
	if(id!=uid)
		sendMsg(id,type:'startScreen')
		return false;
	
	if(!screenVideo.srcObject)
		let options = audio: false, video: true;
		navigator.mediaDevices.getDisplayMedia(options)
		.then(stream => 
			screenVideo.srcObject = stream;
			for(let i in remotes)
				onmessage(uid:i,t:2,type:'s_join');
			
			stream.getVideoTracks()[0].addEventListener('ended', () => 
				closeScreen();
			);
		) 
	

function selfCloseScreen(ot)
	screenVideo.srcObject.getVideoTracks()[0].stop()
	for(let i in remotes)
		sendMsg(i,type:'closeScreen',ot:ot)
	
	screenVideo.srcObject=null;

// 关闭屏幕共享
function closeScreen(id,ot)
	id=getUid(id);
	ot=(ot?ot:1);
	if(id!=uid)
		if(ot==1&&remotes[id].screenVideo)
			remotes[id].screenVideo.srcObject=null;
		else
			sendMsg(id,type:'closeScreen',ot:2)
		
		return false;
	
	if(screenVideo.srcObject&&ot==1)
		selfCloseScreen(ot)
	

// 开启视频
function startVideo(id)
	id=getUid(id);
	if(id!=uid)
		sendMsg(id,type:'startVideo')
		return false;
	
	let v = localVideo.srcObject.getVideoTracks();
	if(v&&v.length>0&&!v[0].enabled)
		v[0].enabled=true;
	

// 关闭视频
function closeVideo(id)
	id=getUid(id);
	if(id!=uid)
		sendMsg(id,type:'closeVideo')
		return false;
	
	let v = localVideo.srcObject.getVideoTracks();
	if(v&&v.length>0&&v[0].enabled)
		v[0].enabled=false;
	

// 开启语音
function startAudio(id)
	id=getUid(id);
	if(id!=uid)
		sendMsg(id,type:'startAudio')
		return false;
	
	let v = localVideo.srcObject.getAudioTracks();
	if(v&&v.length>0&&!v[0].enabled)
		v[0].enabled=true;
	

// 关闭语音
function closeAudio(id)
	id=getUid(id);
	if(id!=uid)
		sendMsg(id,type:'closeAudio')
		return false;
	
	let v = localVideo.srcObject.getAudioTracks();
	if(v&&v.length>0&&v[0].enabled)
		v[0].enabled=false;
	

// 存储通信方信息 
const remotes = 
// 本地视频预览 
const localVideo = document.querySelector('#localVideo')
// 视频列表区域 
const videos = document.querySelector('#videos')
// 同屏视频预览 
const screenVideo = document.querySelector('#screenVideo')
// 同屏视频列表区域 
const screenVideos = document.querySelector('#screenVideos')
// 用户ID
var uid = new Date().getTime() + '';
var ws = new WebSocket('wss://127.0.0.1/myim');
ws.onopen = function() 
	heartBeat();
	onopen();

// 心跳保持ws连接
function heartBeat()
	setInterval(()=>
		ws.send(JSON.stringify( t: 0 ))
	,3000);

function onopen() 
	navigator.mediaDevices
	.getUserMedia(
		audio: true, // 本地测试防止回声 
		video: true
	)
	.then(stream => 
		localVideo.srcObject = stream;
		ws.send(JSON.stringify( t: 1, uid: uid ));
		ws.onmessage = function(event) 
			onmessage(JSON.parse(event.data));
		
	) 

async function onmessage(message) 
	if(message.t==9)
		onleave(message.uid);
	
	if(message.t==2&&message.type)
	switch (message.type) 
		case 'join': 
			// 有新的人加入就重新设置会话,重新与新加入的人建立新会话 
			createRTC(message.uid,localVideo.srcObject,1)
			const pc = remotes[message.uid].pc
			const offer = await pc.createOffer()
			pc.setLocalDescription(offer)
			sendMsg(message.uid,  type: 'offer', offer )
			if(screenVideo.srcObject)
				onmessage(uid:message.uid,t:2,type:'s_join');
			
			break
		
		case 'offer': 
			createRTC(message.uid,localVideo.srcObject,1)
			const pc = remotes[message.uid].pc
			pc.setRemoteDescription(new RTCSessionDescription(message.offer))
			const answer = await pc.createAnswer()
			pc.setLocalDescription(answer)
			sendMsg(message.uid,  type: 'answer', answer )
			break
		
		case 'answer': 
			const pc = remotes[message.uid].pc
			pc.setRemoteDescription(new RTCSessionDescription(message.answer))
			break
		
		case 'candidate': 
			const pc = remotes[message.uid].pc
			pc.addIceCandidate(new RTCIceCandidate(message.candidate))
			break
		
		case 's_join': 
			createRTC(message.uid,screenVideo.srcObject,2)
			const pc = remotes[message.uid].s_pc
			const offer = await pc.createOffer()
			pc.setLocalDescription(offer)
			sendMsg(message.uid,  type: 's_offer', offer )
			break
		
		case 's_offer': 
			createRTC(message.uid,screenVideo.srcObject,2)
			const pc = remotes[message.uid].s_pc
			pc.setRemoteDescription(new RTCSessionDescription(message.offer))
			const answer = await pc.createAnswer()
			pc.setLocalDescription(answer)
			sendMsg(message.uid,  type: 's_answer', answer )
			break
		
		case 's_answer': 
			const pc = remotes[message.uid].s_pc
			pc.setRemoteDescription(new RTCSessionDescription(message.answer))
			break
		
		case 's_candidate': 
			const pc = remotes[message.uid].s_pc
			pc.addIceCandidate(new RTCIceCandidate(message.candidate))
			break
		
		case 'startScreen': 
			startScreen()
			break
		
		case 'closeScreen': 
			const ot = message.ot
			if(ot==1)
				closeScreen(message.uid,1)
			else
				closeScreen(uid,1)
			
			break
		
		case 'startVideo': 
			startVideo()
			break
		
		case 'closeVideo': 
			closeVideo()
			break
		
		case 'startAudio': 
			startAudio()
			break
		
		case 'closeAudio': 
			closeAudio()
			break
		
		default:
			console.log(message)
			break
	

function removeScreenVideo(id)
	if(remotes[id].s_pc)
		remotes[id].s_pc.close()
		if(remotes[id].screenVideo)
		screenVideos.removeChild(remotes[id].screenVideo)
	

function onleave(id) 
	if (remotes[id]) 
		remotes[id].pc.close()
		videos.removeChild(remotes[id].video)
		removeScreenVideo(id)
		delete remotes[id]
	

function leave() 
	ws.send(JSON.stringify( t: 9, uid: uid ));


// socket发送消息 
function sendMsg(tid, msg) 
	msg.t = 2;
	msg.tid=tid;
	msg.uid=uid;
	ws.send(JSON.stringify(msg))

// 创建RTC对象,一个RTC对象只能与一个远端连接 
function createRTC(id,stream,type) 
	const pc = new RTCPeerConnection(
		iceServers: [
			
				urls: 'stun:stun.l.google.com:19302'
			
		]
	)

	// 获取本地网络信息,并发送给通信方 
	pc.addEventListener('icecandidate', event => 
		if (event.candidate) 
			// 发送自身的网络信息到通信方 
			sendMsg(id, 
				type: (type==1?'candidate':'s_candidate'),
				candidate: 
					sdpMLineIndex: event.candidate.sdpMLineIndex,
					sdpMid: event.candidate.sdpMid,
					candidate: event.candidate.candidate
				
			)
		
	)

	// 有远程视频流时,显示远程视频流 
	pc.addEventListener('track', event => 
		if(type==1)
			if(!remotes[id].video)
				const video = createVideo()
				videos.append(video)
				remotes[id].video=video
			
			remotes[id].video.srcObject = event.streams[0]
		else
			if(!remotes[id].screenVideo)
				const video = createVideo()
				screenVideos.append(video)
				remotes[id].screenVideo=video
			
			remotes[id].screenVideo.srcObject = event.streams[0]
		
	)

	// 添加本地视频流到会话中 
	if(stream)
		stream.getTracks().forEach(track => pc.addTrack(track, stream))
	

	if(!remotes[id])remotes[id]=
	if(type==1)
		remotes[id].pc=pc
	else
		remotes[id].s_pc=pc
	

function createVideo()
	const video = document.createElement('video')
	video.setAttribute('autoplay', true)
	video.setAttribute('playsinline', true)
	return video

</script>
</html>

Nginx配置

上面的index.html文件放到D盘根目录下了,然后配置一下websocket

    server 
        listen       443 ssl;
        server_name    mytest.com;
    
        ssl_certificate      lee/lee.crt;
        ssl_certificate_key  lee/lee.key;
    
        ssl_session_cache    shared:SSL:1m;
        ssl_session_timeout  5m;
    
        ssl_ciphers  HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers  on;
    
        location / 
            root   d:/;
            index  index.html index.htm index.php;
        
    
        location /myim 
            proxy_pass http://127.0.0.1:8321/myim;
        
    

运行 

java启动

java -jar cc-im.jar

网页访问

https://127.0.0.1/index.html

带有 socket.io 的 WebRTC/nodejs 中的屏幕共享问题

】带有socket.io的WebRTC/nodejs中的屏幕共享问题【英文标题】:ScreensharingissueinWebRTC/nodejswithsocket.io【发布时间】:2021-11-0306:17:00【问题描述】:我正在开发一个视频会议应用程序,但我似乎无法让屏幕共享工作,出现错误“mediaTypeE... 查看详情

webrtc音视频之ios屏幕共享画面静止时,如何传递视频数据(代码片段)

在开发音视频项目时,涉及到将iOS的屏幕共享画面传递给他人观看,在iOS和Android端可以用一些通知下发,然后正常显示画面,但是微信小程序的音视频开发,无法正常显示,尤其是当屏幕共享是一个静止... 查看详情

腾创网络-webrtc视频会议软件

培训模式在会场过程中,通过举手发言,排队,文档共享,电子白板,手写白板,屏幕共享,多媒体视频文件等数据交互模式功能实现培训。通过举手发言授权,每次仅有1个主讲人固定说话和举手发言... 查看详情

电子中的 WebRTC 屏幕共享

】电子中的WebRTC屏幕共享【英文标题】:WebRTCScreensharinginelectron【发布时间】:2018-02-0812:26:50【问题描述】:我们有一个网络应用程序,我通过在browserWindow中加载URL来构建电子应用程序。但是当我尝试共享屏幕时,它会给我一个... 查看详情

使用 webrtc 时 ReplayKit 不起作用

】使用webrtc时ReplayKit不起作用【英文标题】:ReplayKitdoesn\'tworkwhenworkwithwebrtc【发布时间】:2017-08-2812:42:44【问题描述】:我使用WebRtc创建点对点连接来共享视频和音频。我想使用replaykit录制屏幕和麦克风。如果我在建立对等连接... 查看详情

Android无法在通信过程中从相机切换到屏幕共享webrtc

】Android无法在通信过程中从相机切换到屏幕共享webrtc【英文标题】:Androidunabletoswitchfromcameratoscreensharingwebrtcinmiddleofcommunication【发布时间】:2020-11-2504:38:08【问题描述】:我正在开发android应用程序并将webrtc与openvidu一起使用。... 查看详情

webrtc调用时的音视频流控制

】webrtc调用时的音视频流控制【英文标题】:Videoandaudiostreamcontrolduirngthewebrtccall【发布时间】:2014-02-2518:44:32【问题描述】:我可以通过视频和音频流在2方之间进行webrtc通话。有什么方法可以让用户在通话期间停止仅共享视频... 查看详情

web技术分享|webrtc实现屏幕共享(代码片段)

前言屏幕共享在工作中是非常实用的功能,比如开会时分享ppt,数据图表等,老师也可以分享自己的屏幕,实现线上教学,那么屏幕共享是如何实现的呢,今天就跟随笔者一起研究一下吧!获取button元... 查看详情

4┃音视频直播系统之浏览器中通过webrtc进行桌面共享(代码片段)

🚀优质资源分享🚀学习路线指引(点击解锁)知识定位人群定位🧡Python实战微信订餐小程序🧡进阶级本课程是pythonflask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订... 查看详情

如何实现ios开发webrtc视频通话时录像,截屏。

参考技术A实现iOS开发webrtc视频通话时录像,截屏推荐ZEGO即构科技,4行代码,30分钟在APP、Web和小程序等应用内实现视频通话、语音通话,互动直播功能。【点击免费试用,0成本启动】实现iOS开发webrtc视频通话时录像,截屏的具... 查看详情

4┃音视频直播系统之浏览器中通过webrtc进行桌面共享

做在线直播共享桌面很重要做在线直播共享桌面很重要 查看详情

视频会议软件如何进行屏幕共享

...不错。中目视频会议软件有如下功能:1、音视频功能:语音互动,视频互动,支持外接,音视频设备,支持双流双显/三显。2、协作功能:文字聊天,分组讨论,远程控制,录制等。3、二次开发:API,SDK对接,实现企业整体解决... 查看详情

androidwebview加载webrtc视频通信的问题

参考技术A功能实现情况:通过webrtc实现手机端和PC端视频语音通信;手机端通过webview加载和调用摄像头显示视频窗口问题:在局域网内视频和语音通信正常;公网测试时,手机端连接时间过长(几分钟后),就与服务器端断开连... 查看详情

求一款支持屏幕共享的视频会议软件

...序,共享白板,支持iphone/ipad镜像。2、音视频功能。支持语音互动,视频互动,支持外接,音视频设备,支持双流双显/三显。3、二次开发。支持API,SDK对接,实现企业整体解决方案。4、协作功能。支持文字聊天,分组讨论,远... 查看详情

如何在自动化中验证音频/视频流

...015-11-0207:39:26【问题描述】:我有一个屏幕共享应用程序(WebRTC)。一位用户想要与另一位用户共享他的屏幕。就像在用户1机器上运行的一个应用程序和在用户2机器上运行的另一个应用程序一样。用户1想要共享他的屏幕,现在如... 查看详情

如何实现webrtc多人视频直播会议?

参考技术Awebrtc多人视频直播会议推荐ZEGO即构科技。只需4行代码,30分钟在APP、Web和小程序等应用内实现视频通话、语音通话,互动直播功能。【点击免费试用,0成本启动】webrtc多人视频直播会议实现原理:1、服务端是用C++配... 查看详情

webrtc测试小工具

webrtc:webreal-timecommunication,网页即时通信,是一个支持网页浏览器进行实时语音对话或视频对话(简单的说,就是在web浏览器里面引入实时通信,包括音视频通话等)的API(NativeC++API,webAPI)。谷歌开源,属于W3C推荐标准。支持跨... 查看详情

zoom共享屏幕时能看到别人吗

...挺不错的选择。中目产品功能介绍如下:1、音视频功能语音互动,视频互动,支持外接,音视频设备,支持双流双显/三显。2、协作功能文字聊天,分组讨论,远程控制,录制等。3、二次开发API,SDK对接,实现企业整体解决方案... 查看详情