(原) Webrtc学习:网页视频

原创文章,请后转载,并注明出处。

浏览器的功能是越来越强了。之前做了一个在线聊天室(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>