查看原文
其他

老大:搞一个实时的视频弹幕交互功能?我用websocket+netty搞定了~

点击关注 👉 Java面试那些事儿 2022-04-29

大家好,我是D哥

点击关注下方公众号,Java面试资料 都在这里

来源:binhao.blog.csdn.net/article/details/112631642


2021年了,还有不支持弹幕的视频网站吗,现在各种弹幕玩法层出不穷,抽奖,ppt都上弹幕玩法了,不整个弹幕都说不过去了,今天笔者就抽空做了一个实时视频弹幕交互功能的实现,不得不说这样的形式为看视频看直播,讲义PPT,抽奖等形式增加了许多乐趣。


# 技术选型


netty


官方对于netty的描述:

https://netty.io/

主要关键词描述:netty是异步事件驱动网络框架,可做各种协议服务端,并且支持了FTP,SMTP,HTTP等很多协议,并且性能,稳定性,灵活性都很棒。



可以看到netty整体架构上分了三个部分:


  • 以零拷贝,一致性接口,扩展事件模型的底层核心。
  • Socket,Datagram,Pipe,Http Tunnel作为传输媒介。
  • 传输支持的各种协议,HTTP&WebSocket,SSL,大文件,zlib/gzip压缩,文本,二进制,Google Protobuf等各种各种的传输形式。


 WebSocket


WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。


WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。


为什么做这样的技术选型


  • 由上述可知,实时直播交互作为互动式是一个双向数据传输过程。所以使用webSocket。
  • netty本身支持了webSocket协议的实现,让实现更加简单方便。


# 实现思路


服务架构

整体架构是所有客户端都和我的服务端开启一个双向通道的架构。



传输流程



# 实现效果


视频展示


先看看效果吧,是不是perfect,接下来就来看具体代码是怎么实现的吧。


视频直播弹幕示例

# 代码实现


项目结构


一个maven项目,将代码放一个包下就行。



Java服务端


Java服务端代码,总共三个类,Server,Initailizer和 Handler。


先做一个netty nio的服务端:


一个nio的服务,开启一个tcp端口。

import io.netty.bootstrap.ServerBootstrap;import io.netty.channel.ChannelFuture;import io.netty.channel.EventLoopGroup;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.nio.NioServerSocketChannel;
/** * Copyright(c)lbhbinhao@163.com * @author liubinhao * @date 2021/1/14 * ++++ ______ ______ ______ * +++/ /| / /| / /| * +/_____/ | /_____/ | /_____/ | * | | | | | | | | | * | | | | | |________| | | * | | | | | / | | | * | | | | |/___________| | | * | | |___________________ | |____________| | | * | | / / | | | | | | | * | |/ _________________/ / | | / | | / * |_________________________|/b |_____|/ |_____|/ */public enum BulletChatServer { /** * Server instance */ SERVER;
private BulletChatServer(){ EventLoopGroup mainGroup = new NioEventLoopGroup(); EventLoopGroup subGroup = new NioEventLoopGroup(); ServerBootstrap server = new ServerBootstrap(); server.group(mainGroup,subGroup) .channel(NioServerSocketChannel.class) .childHandler(new BulletChatInitializer()); ChannelFuture future = server.bind(9123); }
public static void main(String[] args) {
}
}

服务端的具体处理逻辑

import io.netty.channel.ChannelInitializer;import io.netty.channel.ChannelPipeline;import io.netty.channel.socket.SocketChannel;import io.netty.handler.codec.http.HttpObjectAggregator;import io.netty.handler.codec.http.HttpServerCodec;import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;import io.netty.handler.stream.ChunkedWriteHandler;import io.netty.handler.timeout.IdleStateHandler;
/** * Copyright(c)lbhbinhao@163.com * * @author liubinhao * @date 2021/1/14 * ++++ ______ ______ ______ * +++/ /| / /| / /| * +/_____/ | /_____/ | /_____/ | * | | | | | | | | | * | | | | | |________| | | * | | | | | / | | | * | | | | |/___________| | | * | | |___________________ | |____________| | | * | | / / | | | | | | | * | |/ _________________/ / | | / | | / * |_________________________|/b |_____|/ |_____|/ */
public class BulletChatInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new HttpServerCodec()); pipeline.addLast(new ChunkedWriteHandler()); pipeline.addLast(new HttpObjectAggregator(1024*64)); pipeline.addLast(new IdleStateHandler(8, 10, 12)); pipeline.addLast(new WebSocketServerProtocolHandler("/lbh")); pipeline.addLast(new BulletChatHandler()); }}

后台处理逻辑,接受到消息,写出到所有的客户端:

import io.netty.channel.Channel;import io.netty.channel.ChannelHandler;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.SimpleChannelInboundHandler;import io.netty.channel.group.ChannelGroup;import io.netty.channel.group.DefaultChannelGroup;import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;import io.netty.util.concurrent.EventExecutorGroup;import io.netty.util.concurrent.GlobalEventExecutor;
/** * Copyright(c)lbhbinhao@163.com * * @author liubinhao * @date 2021/1/14 * ++++ ______ ______ ______ * +++/ /| / /| / /| * +/_____/ | /_____/ | /_____/ | * | | | | | | | | | * | | | | | |________| | | * | | | | | / | | | * | | | | |/___________| | | * | | |___________________ | |____________| | | * | | / / | | | | | | | * | |/ _________________/ / | | / | | / * |_________________________|/b |_____|/ |_____|/ */
public class BulletChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> { // 用于记录和管理所有客户端的channel public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); @Override protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception { // 获取客户端传输过来的消息 String content = msg.text(); System.err.println("收到消息:"+ content); channels.writeAndFlush(new TextWebSocketFrame(content)); System.err.println("写出消息完成:"+content); }
@Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { channels.add(ctx.channel()); }
@Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
String channelId = ctx.channel().id().asShortText(); System.out.println("客户端被移除,channelId为:" + channelId); channels.remove(ctx.channel()); }
@Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); // 发生异常之后关闭连接(关闭channel),随后从ChannelGroup中移除 ctx.channel().close(); channels.remove(ctx.channel()); }
}

网页客户端实现

<!DOCTYPE html><html lang="en"><head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>Netty视频弹幕实现 Author:Binhao Liu</title> <link rel="stylesheet" href=""> <style type="text/css" media="screen"> * { margin: 0px; padding: 0px }
html, body { height: 100% }
body { overflow: hidden; background-color: #FFF; text-align: center; }
.flex-column { display: flex; flex-direction: column; justify-content: space-between;, align-items: center; }
.flex-row { display: flex; flex-direction: row; justify-content: center; align-items: center; }
.wrap { overflow: hidden; width: 70%; height: 600px; margin: 100px auto; padding: 20px; background-color: transparent; box-shadow: 0 0 9px #222; border-radius: 20px; }
.wrap .box { position: relative; width: 100%; height: 90%; background-color: #000000; border-radius: 10px }
.wrap .box span { position: absolute; top: 10px; left: 20px; display: block; padding: 10px; color: #336688 }
.wrap .send { display: flex; width: 100%; height: 10%; background-color: #000000; border-radius: 8px }
.wrap .send input { width: 40%; height: 60%; border: 0; outline: 0; border-radius: 5px 0px 0px 5px; box-shadow: 0px 0px 5px #d9d9d9; text-indent: 1em }
.wrap .send .send-btn { width: 100px; height: 60%; background-color: #fe943b; color: #FFF; text-align: center; border-radius: 0px 5px 5px 0px; line-height: 30px; cursor: pointer; }
.wrap .send .send-btn:hover { background-color: #4cacdc }</style></head><script> var ws = new WebSocket("ws://localhost:9123/lbh");
ws.onopen = function () { // Web Socket 已连接上,使用 send() 方法发送数据 alert("数据发送中..."); }; ws.onmessage = function (e) { console.log("接受到消息:"+e.data); createEle(e.data); }; ws.onclose = function () { // 关闭 websocket alert("连接已关闭..."); }; function sendMsg(msg) { ws.send(msg) }</script><body><div class="wrap flex-column"> <div class="box"> <video src="shape.mp4" width="100%" height="100%" controls autoplay></video> </div> <div class="send flex-row">
<input type="text" class="con" placeholder="弹幕发送[]~(^v^)~*"/>
<div class="send-btn" onclick="javascript:sendMsg(document.querySelector('.con').value)">发送</div> </div></div><script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js" type="text/javascript"></script><script> //1.获取元素 var oBox = document.querySelector('.box'); //获取.box元素 var cW = oBox.offsetWidth; //获取box的宽度 var cH = oBox.offsetHeight; //获取box的高度 function createEle(txt) { //动态生成span标签 var oMessage = document.createElement('span'); //创建标签 oMessage.innerHTML = txt; //接收参数txt并且生成替换内容 oMessage.style.left = cW + 'px'; //初始化生成位置x oBox.appendChild(oMessage); //把标签塞到oBox里面 roll.call(oMessage, { //call改变函数内部this的指向 timing: ['linear', 'ease-out'][~~(Math.random() * 2)], color: '#' + (~~(Math.random() * (1 << 24))).toString(16), top: random(0, cH), fontSize: random(16, 32) }); }
function roll(opt) { //弹幕滚动 //如果对象中不存在timing 初始化 opt.timing = opt.timing || 'linear'; opt.color = opt.color || '#fff'; opt.top = opt.top || 0; opt.fontSize = opt.fontSize || 16; this._left = parseInt(this.offsetLeft); //获取当前left的值 this.style.color = opt.color; //初始化颜色 this.style.top = opt.top + 'px'; this.style.fontSize = opt.fontSize + 'px'; this.timer = setInterval(function () { if (this._left <= 100) { clearInterval(this.timer); //终止定时器 this.parentNode.removeChild(this); return; //终止函数 } switch (opt.timing) { case 'linear': //如果匀速 this._left += -2; break; case 'ease-out': // this._left += (0 - this._left) * .01; break; } this.style.left = this._left + 'px'; }.bind(this), 1000 / 60); }
function random(start, end) { //随机数封装 return start + ~~(Math.random() * (end - start)); }
var aLi = document.querySelectorAll('li'); //10
function forEach(ele, cb) { for (var i = 0, len = aLi.length; i < len; i++) { cb && cb(ele[i], i); } }
forEach(aLi, function (ele, i) { ele.style.left = i * 100 + 'px'; }); //产生闭包 var obj = { num: 1, add: function () { this.num++; //obj.num = 2; (function () { console.log(this.num); }) } }; obj.add();//window</script></body></html>

这样一个实时的视频弹幕功能就完成啦,是不是很简单,各位小伙伴快来试试吧。


# 小结


上班撸代码,下班继续撸代码写博客,这个还是很简单,笔者写这个的时候一会儿就写完了,不过这也得益于笔者很久以前就写过netty的服务,对于Http,Tcp之类协议也比较熟悉,只有前端会有些难度,问下度娘,也很快能做完,在此分享出来与诸君分享,有问题可找笔者交流。



技术交流群


最后,D哥也建了一个技术群,主要探讨一些新的技术和开源项目值不值得去研究及IDEA使用的“骚操作”,有兴趣入群的同学,可长按扫描下方二维码,一定要备注:城市+昵称+技术方向,根据格式备注,可更快被通过且邀请进群。


▲长按扫描


热门推荐:

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存