被异地(女)朋友缠着要一起看电影, 翻遍许多资源并没有发现比较好的, 索性自己写一个,
思路
一起看电影要的效果是:
1. 同时间俩人看到的画面相同
2. 一方点暂停(播放), 一方也暂停(播放)
3. 一方能进行同步操作(将自己的画面转为最新)
所以不需要实时地传输视频流对象,只需同步控制video组件即可
代码的问题
1. 同步操作操纵的是video.currentTime, 那么同时会触发video.onseeked事件,
所以无法分清楚触发onseeked事件的是同步操作还是用户主动滑动时间轴(兴许是我太菜),
索性去掉时间轴, 这也导致用户无法滑动时间轴进行前进和后退
2.在谷歌浏览器上回报index.html:176 Uncaught (in promise) DOMException: play()
failed because the user didn't interact with the document first.错误
它禁止了页面加载完毕时自动播放音视频
3.对于防止盗链的视频链接没有做处理, 导致的结果是在本地进行测试是正确的, 在服务器上跑则会
出现403 forbidden问题(我测试的视频就是......)
前端页面的参考: 来源于GitHub
后端使用Netty来实现WebSocket
获取源代码后你需要做
1. var url = "ws://127.0.0.1:9587/hello"; 和 new NettyClientServer(9587).run();
的端口保持一致, 127.0.0.1改为后端地址, 本地就不需要改
后端源代码
<!--pom-->
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.33.Final</version>
</dependency>
</dependencies>
//NettyClientServer.java
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
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.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
/**
* @author 原来你是小幸运
*/
public class NettyClientServer {
private int port;
public NettyClientServer(int port) {
this.port = port;
}
public void run() throws Exception {
EventLoopGroup boosGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boosGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new HttpServerCodec());
//是以块的方式写,添加ChunkedWriteHandler处理器
socketChannel.pipeline().addLast(new ChunkedWriteHandler());
//http数据在传输中是分段的,HttpObjectAggregator作用就是将其聚合
//这就是为什么,当浏览器发送大量数据的时候,会发生多次http请求
socketChannel.pipeline().addLast(new HttpObjectAggregator(8192));
//WebSocketServerProtocolHandler核心的功能是将http协议升级为ws协议,保持长连接
socketChannel.pipeline().addLast(new WebSocketServerProtocolHandler("/hello"));
//添加自定义的handler,.自定义逻辑处理
socketChannel.pipeline().addLast(new ChatGroupServerHandler());
}
});
ChannelFuture channelFuture = bootstrap.bind(port).sync();
channelFuture.channel().closeFuture().sync();
} finally {
boosGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new NettyClientServer(9587).run();
}
}
//ChatGroupServerHandler.java
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.GlobalEventExecutor;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author TextWebSocketFrame:表示一个文本帧
*/
public class ChatGroupServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
/**用于记录和管理所有客户端的channel*/
private static ChannelGroup clients =
new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
/**能否更改newTime,为了防止置0的时候,被别的线程更改*/
private static boolean canChange = true;
private static double newTime = 0;
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
//同步消息, 只是自己同步
if ("同步".equals(textWebSocketFrame.text())) {
//只发给自己 使用channelHandlerContext.channel()
System.out.println("同步消息: " + newTime);
channelHandlerContext.channel().writeAndFlush(
new TextWebSocketFrame("同步" + newTime)
);
} else if (isNumeric(textWebSocketFrame.text())) {
//定时任务传递的时间
if (canChange) {
setCanChange(Double.parseDouble(textWebSocketFrame.text()));
} else {
newTime = 0;
}
}
if ("重新开始播放".equals(textWebSocketFrame.text()) || textWebSocketFrame.text().startsWith("换")) {
//设置newTime
canChange = false;
newTime = -1;
if ( textWebSocketFrame.text().startsWith("换")){
//更换视频 和 重新开始都需要将newTime置0
clients.writeAndFlush(
new TextWebSocketFrame(textWebSocketFrame.text())
);
}else {
clients.writeAndFlush(
new TextWebSocketFrame("重新开始播放")
);
}
newTime = -1;
System.out.println(newTime);
//间隔一秒钟, 等待前端重新开始完毕
Thread.sleep(1000);
System.out.println(newTime);
newTime = 0.1;
canChange = true;
} else {
clients.writeAndFlush(
new TextWebSocketFrame(textWebSocketFrame.text())
);
}
}
//当用户连接的时候触发
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
clients.add(ctx.channel());
System.out.println("有用户连接啦,id是: " + ctx.channel().id().asLongText());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
clients.remove(ctx.channel());
if (clients.size()==0){
newTime = 0;
}
System.out.println("有用户断开连接啦,id是: " + ctx.channel().id().asLongText());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
//判断字符串是不是double
public static boolean isNumeric(String str) {
Pattern pattern = Pattern.compile("[0-9]+[.]{0,1}[0-9]*[dD]{0,1}");
Matcher isNum = pattern.matcher(str);
if (!isNum.matches()) {
return false;
}
return true;
}
public void setCanChange(double msg) {
if (newTime != -1) {
if ((msg - newTime) > 3 && newTime <= 0.1) {
//解决重新播放时, 线程安全问题
return;
}
newTime = Math.max(msg, newTime);
}
}
}
前端源代码
百度网盘:
链接:https://pan.baidu.com/s/1FHiBVOy1hlFk19YIvJ7OaA 提取码:0000
or 蓝奏云:
https://laiyongbin.lanzous.com/iWAIQnpuzxg
后续可升级的操作
1. 进度条 现在进度条在reset.css里15行进行了隐藏
2. 倍速播放
3. 语音通话