2021年了,還有不支持彈幕的視頻網站嗎,現在各種彈幕玩法層出不窮,抽獎,ppt都上彈幕玩法了,不整個彈幕都說不過去了,今天筆者就抽空做了一個實時視頻彈幕交互功能的實現,不得不說這樣的形式爲看視頻看直播,講義PPT,抽獎等形式增加了許多樂趣。
1 技術選型
1.1 netty
官方對於netty的描述:
主要關鍵詞描述: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)[email protected]
* @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)[email protected]
*
* @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)[email protected]
*
* @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