乐趣区

关于java:websocketnetty实时视频弹幕交互功能Java版

2021 年了,还有不反对弹幕的视频网站吗,当初各种弹幕玩法层出不穷,抽奖,ppt 都上弹幕玩法了,不整个弹幕都说不过去了,明天笔者就抽空做了一个实时视频弹幕交互性能的实现,不得不说这样的模式为看视频看直播,讲义 PPT,抽奖等模式减少了许多乐趣。

1 技术选型

1.1 netty
官网对于 netty 的形容:

https://netty.io/

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

能够看到 netty 整体架构上分了三个局部:

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

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

WebSocket 使得客户端和服务器之间的数据交换变得更加简略,容许服务端被动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只须要实现一次握手,两者之间就间接能够创立持久性的连贯,并进行双向数据传输。

1.3 为什么做这样的技术选型。
由上述可知,实时直播交互作为互动式是一个双向数据传输过程。所以应用 webSocket。
netty 自身反对了 webSocket 协定的实现,让实现更加简略不便。

2 实现思路

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

2.2 传输流程

3 实现成果

3.1 视频展现
先看看成果吧,是不是 perfect,接下来就来看具体代码是怎么实现的吧。

图片视频直播弹幕示例

4 代码实现

4.1 我的项目构造
一个 maven 我的项目,将代码放一个包下就行。

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

4.2.1 先做一个 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) {}}

4.2.2 服务端的具体解决逻辑

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());
    }

}

4.3 网页客户端实现

<!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>

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

5 小结

下班撸代码,上班持续撸代码写博客,这个还是很简略,笔者写这个的时候一会儿就写完了,不过这也得益于笔者很久以前就写过 netty 的服务,对于 Http,Tcp 之类协定也比拟相熟,只有前端会有些难度,问下度娘,也很快能做完,在此分享进去与诸君分享。

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

退出移动版