浏览器的功能是越来越强了。之前做了一个在线聊天室(https://c.scwy.net),可以图片、文字、语音,看起来还可以增加视频。
刚才试了一下网友的演示:https://github.com/243286065/WebRTCDemo, 示例5. remote_chat,在手机和平板之间视频成功。
稍后再学习学习。
另外还看到一个Pion-WebRTC: 纯go语言实现的webrtc框架库,也值得学习使用。
看个与后端没啥关系的:获取本地摄像头
<body>
<script src="js/adapter.js" type="text/javascript"></script>
<script>
var myVideoStream, myVideo;
window.onload = function() {
myVideo = document.getElementById("myVideo");
getMedia();
}
//获取本地媒体
function getMedia() {
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
navigator.getUserMedia({"audio":true, "video":true}, gotUserMedia, didntGetUserMedia);
}
//成功获取媒体
function gotUserMedia(stream) {
myVideoStream = stream;
//显示本地视频
myVideo.srcObject = stream
}
function didntGetUserMedia() {
console.log("couldn't get videp")
}
</script>
<div id="setup">
<P>WebRTC Demo--调用本地摄像头和麦克风</P>
</div>
<br/>
<div style="width: 30%;vertical-align: top;">
<div>
<video id="myVideo" autoplay="autoplay" controls muted="true"></video>
</div>
</div>
</body>
除了adapter.js外,代码还是比较简单。
来看看远程视频的源代码。测试中,当两端建立好了视频连接,服务器已经没有作用:
// 启动了一个Https服务
router := route.Router()
router.RunTLS(config.WebServerHostTLS, "ssl/server.crt", "ssl/server.key")
看看这个route,使用了Gin
router := gin.Default()
router.Static("/static/", "./static") // 处理静态资源
router.GET("/", handler.DefaultHomePageHandler) //处理默认首页
//处理socketio请求
router.GET("/socket.io/", handler.SocketIOServerHandler)
router.POST("/socket.io/", handler.SocketIOServerHandler)
router.Handle("WS", "/socket.io", handler.SocketIOServerHandler)
router.Handle("WSS", "/socket.io", handler.SocketIOServerHandler)
在handler.go和signal.go中处理了路径的功能
// 打开首页,只是跳转到了一个静态html
func DefaultHomePageHandler(c *gin.Context) {
c.Redirect(http.StatusFound, "/static/index.html")
}
看起来有点含量的在signal.go中
package handler
import (
"log"
"github.com/gin-gonic/gin"
socketio "github.com/googollee/go-socket.io"
)
var (
server *socketio.Server
err error
)
const (
MaxUserCnt = 2
)
type Msg struct {
UserID string `json:"userID"`
Text string `json:"text"`
State string `json:"state"`
Namespace string `json:"namespace"`
Rooms []string `json:"rooms"`
}
func init() {
server, err = socketio.NewServer(nil)
if err != nil {
log.Fatal(err)
}
server.OnConnect("/", func(so socketio.Conn) error {
log.Println("on connection, ID: ", so.ID())
so.SetContext("")
msg := Msg{so.ID(), "Connected", "notice", "", nil}
so.Emit("res", msg)
return nil
})
server.OnEvent("/", "join", func(so socketio.Conn, room string) {
if server.RoomLen(so.Namespace(), room) >= MaxUserCnt {
//房间已满
so.Emit("full", room)
return
}
//加入房间
so.Join(room)
log.Println(so.ID(), " join ", room, so.Rooms())
//全员发送joined消息,客户端自己判断是否是有新用户加入
server.BroadcastToRoom(so.Namespace(), room, "joined", room, so.ID())
})
//处理用户离开消息
server.OnEvent("/", "leave", func(so socketio.Conn, room string) {
log.Println(so.ID(), " leave ", room, so.Namespace(), so.Rooms())
server.BroadcastToRoom(so.Namespace(), room, "leaved", room, so.ID())
so.Leave(room)
})
server.OnEvent("/", "message", func(so socketio.Conn, room string, msg interface{}) {
//原封不动地转发
server.BroadcastToRoom(so.Namespace(), room, "message", room, so.ID(), msg)
})
server.OnEvent("/", "ready", func(so socketio.Conn, room string) {
//原封不动地转发
server.BroadcastToRoom(so.Namespace(), room, "ready", room, so.ID())
})
server.OnEvent("/", "chat", func(so socketio.Conn, msg string) {
res := Msg{so.ID(), "----" + msg, "normal", so.Namespace(), so.Rooms()}
so.SetContext(res)
log.Println("chat receive", msg, so.Namespace(), so.Rooms(), server.Rooms(so.Namespace()))
rooms := so.Rooms()
for _, room := range rooms {
server.BroadcastToRoom(so.Namespace(), room, "res", res)
}
})
go server.Serve()
}
func SocketIOServerHandler(c *gin.Context) {
//server.OnEvent("/", "notice")
if server != nil {
log.Println("WebSocket server start...")
server.ServeHTTP(c.Writer, c.Request)
}
}
除了新出现的Join,BroadcastToRoom,Leave等,总体还是比较好理解。
看看前端
<html>
<head>
<title>远程1vs1视频聊天Demo</title>
<style>
video {
width: 480px;
height: 320px;
border: 1px solid black;
}
div {
display: inline-block;
}
</style>
<script type="text/javascript" src="../js/jquery.min.js"></script>
<script src="/static/js/socket.io.js"></script>
</head>
<body>
<div style="width: 100%;vertical-align: top;">
<div>
<video id="localVideo" autoplay="autoplay" playsinline="true" controls muted></video>
<video id="remoteVideo" playsinline="true" autoplay="autoplay" controls style="margin-left: 20px;"></video>
</div>
</div>
<button id="joinBtn">加入房间</button>
<button id="leaveBtn" disabled="true">离开房间</button>
<button id="downloadBtn" disabled="">下载</button>
<script>
//各个控件
var localVideo = document.getElementById('localVideo');
var remoteVideo = document.getElementById('remoteVideo');
var joinButton = $("#joinBtn");
var leaveButton = $("#leaveBtn");
var room = "";
var state = "offline";
var socket = null;
var roomId = -1;
var targetSocket = null;
var pcConfig = {
'iceServers': [{
url: 'stun:stun.l.google.com:19302',
}]
}
var pc = null; //本地PeerConnection对象
var localStream = null;
var remoteStream = null;
var offerDesc = null;
joinButton.click(function () {
room = prompt("请输入房间号:")
if (room == "") {
return;
}
joinButton.attr('disabled', true);
leaveButton.attr('disabled', false);
doJoinRoom(room);
});
leaveButton.click(function () {
joinButton.attr('disabled', false);
leaveButton.attr('disabled', true);
doLeaveRoom();
});
//加入房间
function doJoinRoom(room) {
//初始化socketio
var serverHost = "https://" + window.location.host;
socket = io(serverHost)
console.log(socket);
// if(socket > 0) {
// console.log("Connect server succ:", socket);
// } else {
// console.error('Failed to connect signal server', socket);
// }
setupSocketIO();
socket.emit("join", room);
}
function setupSocketIO() {
//设置socket消息处理
socket.on("joined", (roomid, socketid) => {
console.log("recieve msg: ", roomid, " ", socketid);
roomId = roomid;
if (socket.id == socketid) {
//发送给自己的消息,自己加入
//获取本地stream
var constraints = {
video: {
width: 640,
height: 480
},
audio: {
echoCancellation: true,
noiseSupperssion: true,
autoGainControl: true
}
};
navigator.mediaDevices.getUserMedia(constraints)
.then(getMediaStream)
.catch(handleError);
} else {
//有新用户加入,与它建立连接
console.log('new user joined room: ', socketid);
//此时对端可能还没有初始化好,因此需要再加一步
if(targetSocket == null) {
targetSocket = socketid;
}
}
});
socket.on("ready", (roomid, socketid)=> {
//对端准备好了才开始会话
if(socketid == targetSocket) {
console.log("target reday");
startCall();
}
})
socket.on("full", (roomid)=>{
alert("房间已满:" + roomid);
socket.disconnect();
socket = null;
});
socket.on('leaved', (roomid, socketid)=> {
console.log("user leave room: " + socketid);
if(socketid == socket.id) {
hangup();
socket.disconnect();
socket = null;
localVideo.srcObject = null;
} else {
remoteVideo.srcObject = null;
targetSocket = null;
//重新初始化本地的PeerConnection
hangup();
initLocal();
}
});
socket.on('message', (roomid, socketid, msg)=>{
if(msg === null || msg === undefined) {
console.error("Recieve invalid message");
return;
}
//本机发送的数据忽略
if(socketid == socket.id) {
return;
}
console.log(pc);
if(msg.hasOwnProperty('type') && msg.type === 'offer') {
pc.setRemoteDescription(new RTCSessionDescription(msg));
//创建anwser
pc.createAnswer().then(getAnswer).catch(handleAnswerError);
} else if(msg.hasOwnProperty('type') && msg.type ==='answer') {
pc.setRemoteDescription(new RTCSessionDescription(msg));
} else if(msg.hasOwnProperty('type') && msg.type === 'candidate') {
console.log("candidate:", msg);
var candidate = new RTCIceCandidate({
sdpMLineIndex: msg.label,
candidate: msg.candidate
});
pc.addIceCandidate(candidate);
} else {
console.log('Invalid message', msg);
}
});
}
//离开房间
function doLeaveRoom() {
if(socket) {
//alert(roomId);
socket.emit("leave", roomId);
} else {
console.error("socket not init!");
}
}
function getMediaStream(stream) {
if (localStream) {
stream.getAudioTracks().forEach((track) => {
localStream.addTrack(track);
stream.removeTrack(track);
});
} else {
localStream = stream;
}
localVideo.srcObject = localStream;
initLocal();
return;
}
function initLocal() {
//创建本地PeerConnection
createPeerConnection();
//绑定track
bindTracks();
//通知对端自己准备好
socket.emit("ready", roomId);
}
function handleError(err) {
console.error('Failed to get media stream: ', err);
}
//创建PeerConnection
function createPeerConnection() {
console.log("create RTCPeerConnection");
if (!pc) {
pc = new RTCPeerConnection(pcConfig);
pc.onicecandidate = (e) => {
if (e.candidate) {
sendMessage(roomId, {
type: 'candidate',
label: event.candidate.sdpMLineIndex,
id: event.candidate.sdpMid,
candidate: event.candidate.candidate
});
} else {
console.log("end candidate");
}
};
pc.ontrack = getRemoteStream;
}
}
function bindTracks() {
console.log("bind tracks into RTCPeerConnection");
if (pc === null || localStream === null || localStream === undefined) {
console.error('pc or localStream is null or undefined');
return;
}
localStream.getTracks().forEach((track) => {
pc.addTrack(track, localStream);
});
}
function getRemoteStream(e) {
console.log("getremoteStream")
remoteStream = e.streams[0];
remoteVideo.srcObject = remoteStream;
}
function hangup() {
if(!pc) {
return;
}
pc.close();
pc = null;
}
function sendMessage(roomid, data) {
if (!socket) {
console.log('socket is null');
return;
}
socket.emit('message', roomid, data);
}
function startCall() {
var offerOptions = {
offerToRecieveAudio :1,
offerToRecieveVideo:1
}
pc.createOffer(offerOptions).then(getOffer).catch(handleOfferError);
}
//创建offer成功
function getOffer(sdp) {
pc.setLocalDescription(sdp);
offerDesc = sdp;
//发送给对端
sendMessage(roomId, offerDesc);
}
function handleOfferError(error) {
console.error('create offer failed: ', error);
}
function getAnswer(sdp) {
pc.setLocalDescription(sdp);
sendMessage(roomId, sdp);
}
function handleAnswerError(error) {
console.error('create answer failed: ', error);
}
</script>
</body>
</html>