老板怒吼:今晚整一個B站彈幕交互功能
圖片來自 包圖網
今天筆者就抽空做了一個實時視頻彈幕交互的小功能,不得不說這樣的形式為看視頻看直播,講義 PPT,抽獎等形式增加了許多樂趣。
技術選型
①Netty
官方對于 Netty 的描述:
- https://netty.io/
主要關鍵詞描述:Netty 是異步事件驅動網絡框架,可做各種協(xié)議服務端,并且支持了 FTP,SMTP,HTTP 等很多協(xié)議,并且性能,穩(wěn)定性,靈活性都很棒。
可以看到 Netty 整體架構上分了三個部分:
- 以零拷貝,一致性接口,擴展事件模型的底層核心。
- Socket,Datagram,Pipe,Http Tunnel 作為傳輸媒介。
- 傳輸支持的各種協(xié)議,HTTP&WebSocket,SSL,大文件,zlib/gzip 壓縮,文本,二進制,Google Protobuf 等各種各種的傳輸形式。
②WebSocket
WebSocket 是一種在單個 TCP 連接上進行全雙工通信的協(xié)議。WebSocket 通信協(xié)議于 2011 年被 IETF 定為標準 RFC 6455,并由 RFC7936 補充規(guī)范。
WebSocket API 也被 W3C 定為標準。WebSocket 使得客戶端和服務器之間的數(shù)據(jù)交換變得更加簡單,允許服務端主動向客戶端推送數(shù)據(jù)。
在 WebSocket API 中,瀏覽器和服務器只需要完成一次握手,兩者之間就直接可以創(chuàng)建持久性的連接,并進行雙向數(shù)據(jù)傳輸。
為什么做這樣的技術選型:
- 由上述可知,實時直播交互作為互動式是一個雙向數(shù)據(jù)傳輸過程。所以使用 WebSocket。
- Netty 本身支持了 WebSocket 協(xié)議的實現(xiàn),讓實現(xiàn)更加簡單方便。
實現(xiàn)思路
①服務架構
整體架構是所有客戶端都和我的服務端開啟一個雙向通道的架構。
②傳輸流程
如下圖:
實現(xiàn)效果
先看看效果吧,是不是 perfect,接下來就來看具體代碼是怎么實現(xiàn)的吧。
視頻直播彈幕示例
代碼實現(xiàn)
①項目結構
一個 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();
- // 發(fā)生異常之后關閉連接(關閉channel),隨后從ChannelGroup中移除
- ctx.channel().close();
- channels.remove(ctx.channel());
- }
- }
③網頁客戶端實現(xiàn)
代碼如下:
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <title>Netty視頻彈幕實現(xiàn) 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() 方法發(fā)送數(shù)據(jù)
- alert("數(shù)據(jù)發(fā)送中...");
- };
- 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="彈幕發(fā)送[]~(^v^)~*"/>
- <div class="send-btn" onclick="javascript:sendMsg(document.querySelector('.con').value)">發(fā)送</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) {
- //動態(tài)生成span標簽
- var oMessage = document.createElement('span'); //創(chuàng)建標簽
- oMessage.innerHTML = txt; //接收參數(shù)txt并且生成替換內容
- oMessage.style.left = cW + 'px'; //初始化生成位置x
- oBox.appendChild(oMessage); //把標簽塞到oBox里面
- roll.call(oMessage, {
- //call改變函數(shù)內部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; //終止函數(shù)
- }
- 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) {
- //隨機數(shù)封裝
- 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 之類協(xié)議也比較熟悉。
只有前端會有些難度,問下度娘,也很快能做完,在此分享出來與諸君分享,有問題可找筆者交流。
作者:興趣使然的程序猿
編輯:陶家龍
出處:http://adkx.net/w71wf