H265播放器及设备和服务器端信令交互说明

33 篇文章 5 订阅
24 篇文章 20 订阅
本文详细阐述了一种将H.265播放器与WebRTC信令深度集成的方法,通过MQTT协议管理大量分散设备的P2P拉流,包括信令传输、视频编码与解码、数据通道设置等关键步骤。同时介绍了设备端和浏览器端的信令处理流程,以及不同模式(如VNC、H264、H265)的响应和处理策略。
摘要由CSDN通过智能技术生成

      h265播放器主要针对webrtc的实时流,所以信令在里面是一个很重要的组成部分,由于业务的关系,需要自己将信令跟自己的系统进行深度融合,本播放器,主要针对大量分散设备的管理和p2p拉流,所以主要选择了mqtt这个极简有很灵活的协议作为信令传输的主要协议,为了让大家尽快用到自己的系统中去,现将信令播放器端和设备端实现做一说明。以方便大家可以很快的实现demo。

浏览器端通过建立mqtt连接,然后将自己的offer 以及相关参数发送给受控设备,在收到answer后即可通过建立datachannel 进行H265码流接收

function getStreamWebrtc(player) {


    pc = new RTCPeerConnection({
            iceServers: ICEServerkvm,//ICEServer
    });
    // initH265Transfer(pc,player);
    if(bAudio) {
    // initAudioDC(pc);
        pc.addTransceiver('audio', { direction: 'recvonly' });
        OnTrack(pc)
    }
    if(bVideo) {
        if(!bDecodeH264){
            // media_mode= "h265";
           initH265DC(pc,player);
        }else{
            // media_mode= "h264";
            pc.addTransceiver('video', { direction: 'recvonly' });
            // receivervideo.playoutDelayHint = 0;
            OnTrack(pc)
        }
    }
	// Populate SDP field when finished gathering
	pc.oniceconnectionstatechange = e => {
        log(pc.iceConnectionState)
        if(!bDecodeH264){
                var state ={
                    t: kconnectStatusResponse,
                    s: pc.connectionState
                }
                player.postMessage(state)
        }

    }

    pc.onicecandidate = event => {
            if (event.candidate === null) {
                //pc.setLocalDescription(offer)
                var msgdata = new Object();
                //var localSessionDescription =btoa(JSON.stringify(pc.localDescription));

                msgdata["seqid"] = WEB_SEQID;
                if (bVideo) {
                    msgdata["video"] = true;
                    msgdata["mode"] = media_mode;
                    if (media_mode == "rtsp") {
                        let rtsp = document.getElementById("rtspId");
                        let rtspaddr = rtsp.value;
                        if (rtspaddr == "") {
                            rtspaddr = KVMRTSPADDR;
                            rtsp.value = KVMRTSPADDR;
                        }
                        msgdata["rtspaddr"] = rtspaddr;//document.getElementById("rtspId").value //KVMRTSPADDR;
                    }
                    msgdata["resolution"]=p_Resolution;//document.getElementById("resolutionId").value;
                    
                }
                if (bAudio) {
                    msgdata["audio"] = true;
                    msgdata["mode"] = media_mode;
                }

                msgdata["serial"] = false;//true;
                msgdata["ssh"] = false;//true;
                msgdata["iceserver"] = ICEServerkvm;
                msgdata["offer"] = pc.localDescription;//localSessionDescription;
                msgdata["suuid"] = kvmstream;


                var content = new Object();

                content["type"] = CMDMSG_OFFER;
                content["msg"] = "webrtc offer";
                content["device_id"] = document.getElementById("deviceId").value;
                content["data"] = btoa(JSON.stringify(msgdata));
                mqttclient.publish(pubtopic, JSON.stringify(content));

                console.log("localDescription:", btoa(JSON.stringify(pc.localDescription)));
            }
        }
	
	pc.createOffer().then(d => pc.setLocalDescription(d)).catch(log)   

};
initMqtt = function(player) {
    if(bmqttStarted){
        console.log("mqtt is connect");
        return;
    }
    var ClientId = 'mqttjs_' + Math.random().toString(16).substr(2, 8)
  
    mqttclient = mqtt.connect(MqttServer,
        {
            clientId: ClientId,
   
            username: 'admin',
            password: 'password',
            // port: 8084
        });
    mqttclient.on('connect', function () {
        mqttclient.subscribe(subtopic, function (err) {
            if (!err) {
                //成功连接到服务器
                console.log("connected to server");
                bmqttStarted=true;

                getStreamWebrtc(player);
 
            }
        })
    })
    mqttclient.on('message', function (topic, message,) {
        // message is Buffer
        console.log("topic:",topic)
        console.log("message:",message)

        let input = JSON.parse(message)
        console.log("input:",input)
        switch (input.type) {
            case 'offer': 
              getRemoteOffer(input);
              break;
            case "error":
                console.log("msg:",input.msg);
                // stopSession();
                break;
            case "answer":
                var remoteSessionDescription = input.data;
                if (remoteSessionDescription === '') {
                    alert('Session Description must not be empty');
                }
                try {
                    let answerjsonstr=atob(remoteSessionDescription);
                    console.log("atob1:",answerjsonstr);

                    let answer = JSON.parse(answerjsonstr);
                    console.log("answer:",answer);
                    for (const receiver of pc.getReceivers()) {
                        receiver.playoutDelayHint = 0;
                      }
                    pc.setRemoteDescription(new RTCSessionDescription(answer));
                    // btnOpen();
                } catch (e) {
                    alert(e);
                }
                break;

            case "heart":
                console.log(JSON.parse(atob(input.data)));
                break;
            case "cmdFeedback":
                console.log(JSON.parse(atob(input.data)));
                break;

        }
    })
}

   以下是设备端(或者服务器端)的信令结构体组要结构

type Session struct {
	Type     string `json:"type"`
	Msg      string `json:"msg"`
	Data     string `json:"data"`
	DeviceId string `json:"device_id"`
}
type PublishMsg struct {
	WEB_SEQID string
	Topic     string
	Msg       interface{}
}

// Message
type Message struct {
	SeqID               string                    `json:"seqid"`
	Mode                string                    `json:"mode"`
	Video               bool                      `json:"video"`
	Resolution          string                    `json:"resolution"`
	Serial              bool                      `json:"serial"`
	SSH                 bool                      `json:"ssh"`
	Audio               bool                      `json:"audio"`
	ICEServers          []webrtc.ICEServer        `json:"iceserver"`
	RtcSession          webrtc.SessionDescription `json:"offer" mapstructure:"offer"`
	VideoRtspServerAddr string                    `json:"rtspaddr"`
	Suuid               string                    `json:"suuid"`
}
主要的命令关键字
	CMDMSG_OFFER               = "offer"
	CMDMSG_DISC                = "discovery"
	CMDMSG_WAKE                = "wake"
	CMDMSG_UPDATE              = "update"
	CMDMSG_MR2                 = "mr2"
	CMDMSG_PROCLIST            = "cmdlist"
	CMDMSG_PROCKILL            = "killcmdproc"
	CMDMSG_GETKVMRTSPINFOLIST  = "getkvmrtspinfolist"
	CMDMSG_RESPKVMRTSPINFOLIST = "respkvmrtspinfolist"
	CMDMSG_SWITCHKVMSCR        = "switchrtspinfo"
	CMDMSG_GFILE               = "gfile"  //用gfile进行文件传输
	CMDMSG_FILEGO              = "filego" //采用filego进行文件传输

设备接收到浏览器发来的信令后根据信令的不同type进行分发处理,

func (o *handler) handle(client mqtt.Client, msg mqtt.Message) {
	// We extract the count and write that out first to simplify checking for missing values
	var m Message
	var resp Session
	if err := json.Unmarshal(msg.Payload(), &resp); err != nil {
		fmt.Printf("Message could not be parsed (%s): %s", msg.Payload(), err)
		return
	}
	fmt.Println(resp)
	switch resp.Type {
	case CMDMSG_OFFER:
		enc.Decode(resp.Data, &m)
		Notice(m)
	case CMDMSG_DISC:
		var devcmd DiscoveryCmd
		enc.Decode(resp.Data, &devcmd)
		DiscoveryDev(&devcmd, m)
	case CMDMSG_WAKE:
		var macs Fing
		enc.Decode(resp.Data, &macs)
		go wakemac(macs)
	case CMDMSG_UPDATE:
		var newver versionUpdate
		GetUpdateMyself(&newver, m)
	case CMDMSG_MR2:
		var mr2info Mr2Msg
		enc.Decode(resp.Data, &mr2info)
		go Mr2HostPort(&mr2info, m)
	case CMDMSG_PROCLIST:
		go GetCmdProcList(m)
	case CMDMSG_PROCKILL:
		pid, err := strconv.Atoi(resp.Data)
		if err != nil {
			fmt.Print(err.Error())
		} else {
			go utils.KillCmdProc(pid)
		}
	case CMDMSG_GETKVMRTSPINFOLIST:
		GetKvmRtspInfoList(m)
	case CMDMSG_SWITCHKVMSCR:
		var rtspinfo RTSPInfo
		enc.Decode(resp.Data, &rtspinfo)
		go SwitchSrcIndex(rtspinfo)
	case CMDMSG_GFILE: //文件操作
		// var gfileinfo GFileInfo
		// enc.Decode(resp.Data, &gfileinfo)
		// GFileNotice(gfileinfo)
	case CMDMSG_FILEGO: //采用filego 组件进行文件传输

	}
}

 根据不同的信令进行Mode分发

	VNC               = "vnc"
	H265              = "h265"
	AUDIO_DC          = "audiodc"
	MODE_VNC          = "vnc"
	MODE_RTSP         = "rtsp"
	MODE_H264         = "h264"
	MODE_H265         = "h265"

func Notice(msg Message) {

	switch msg.Mode {

	case MODE_VNC:
		if bUseCGORV1126 {
			go CaputureScreen(msg)
		} else {
			answermsg := PublishMsg{
				WEB_SEQID: msg.SeqID,
				Topic:     "refuse",
				Msg:       "not supported mode" + msg.Mode,
			}
			fmt.Println("answer %s", msg.SeqID)
			SendMsg(answermsg) //response)
		}
		break
	case MODE_H264:
		if bUseCGORV1126 {
			go doSignalingMqttH264(msg)
		} else {

			answermsg := PublishMsg{
				WEB_SEQID: msg.SeqID,
				Topic:     "refuse",
				Msg:       "not supported mode" + msg.Mode,
			}
			fmt.Println("answer %s", msg.SeqID)
			SendMsg(answermsg) //response)
		}
		break
	case MODE_RTSP:
		go doSignalingMqtt(msg)
		break
	case MODE_H265:
		if bUseCGORV1126 {
			go CaputureScreen(msg)
		} else {

			answermsg := PublishMsg{
				WEB_SEQID: msg.SeqID,
				Topic:     "refuse",
				Msg:       "not supported mode" + msg.Mode,
			}
			fmt.Println("answer %s", msg.SeqID)
			SendMsg(answermsg) //response)
		}
		break
	default:
		answermsg := PublishMsg{
			WEB_SEQID: msg.SeqID,
			Topic:     "refuse",
			Msg:       "not supported mode" + msg.Mode,
		}
		fmt.Println("answer %s", msg.SeqID)
		SendMsg(answermsg) //response)
		break
	}
	// go doSignalingMqtt(msg)

	// go CaputureScreen(msg)
}

H265 处理连接建立

func CaputureScreen(msg Message) {
	// doing screen capturing at minimum s.timeout
	//var wg sync.WaitGroup
	// var res *image.RGBA = nil
	if nInSendH265Track > MAXH265DCCONNECTNUMBERS {
		return
	}
	fmt.Println("CaputureScreen msg", msg.Suuid)
	// dcchan = make(chan *webrtc.DataChannel)
	//time.Sleep(5 * time.Millisecond)
	CurrentKVMRTSP.Suuid = msg.Suuid
	CurrentKVMRTSP.URL = msg.VideoRtspServerAddr
	if rtspsrcmap[msg.Suuid] == nil {
		fmt.Println("suuid %s is not exist", msg.Suuid)
		return
	}
	peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{
		ICEServers:   msg.ICEServers,
		SDPSemantics: webrtc.SDPSemanticsUnifiedPlanWithFallback,
	})

	if err != nil {
		logger.Errorf("NewPeerConnection error: %v", err)
		return
	}

	var mediatype string

	if msg.Audio {
		if nInSendAudioTrack >= MAXAUDIOCONNECTNUMBERS {
			fmt.Println("doSignalingMqttH264 msg connect up to MAXCONNECT", MAXAUDIOCONNECTNUMBERS)
			answermsg := PublishMsg{
				WEB_SEQID: msg.SeqID,
				Topic:     "refuse",
				Msg:       "audio connect up to MAXCONNECT ,retry aftermoment",
			}
			fmt.Println("refuse %s", msg.SeqID)
			SendMsg(answermsg) //response)
			return
		}
		if mediatype != "" {
			mediatype = mediatype + "&audio"
		} else {
			mediatype = "audio"
		}
		SendAudio(peerConnection)
	}

	// fmt.Printf("2")
	peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
		if connectionState == webrtc.ICEConnectionStateDisconnected {
			// atomic.AddInt64(&peerConnectionCount, -1)
			if err := peerConnection.Close(); err != nil {
				logger.Errorf("peerConnection.Close error %v", err)
			}
		} else if connectionState == webrtc.ICEConnectionStateConnected {
			// atomic.AddInt64(&peerConnectionCount, 1)
		}
	})
	rtcpcmap[msg.SeqID] = peerConnection
	peerConnection.OnDataChannel(func(dc *webrtc.DataChannel) {
		if dc.Label() == SSH {
			sshDataChannelHandler(dc)
		}
		if dc.Label() == CONTROL {
			controlDataChannelHandler(dc)
		}
		if dc.Label() == SERIAL {
			serialDataChannelHandler(dc)
		}
		if dc.Label() == HID {
			HIDDataChannelHandler(dc, "jpeg")
		}
		if dc.Label() == GFILE {
			GFileDataChannelHandler(dc)
		}
		if dc.Label() == VNC {
			VNCDataChannelHandler(dc)
		}
		if dc.Label() == H265 {
			err = rk1126.StartH264(msg.Resolution, true)
			if err != nil {
				panic(err)
			}
			H265dcmap[msg.SeqID] = dc
			H265DataChannelHandler(dc, mediatype)
		}
		if dc.Label() == AUDIO_DC {
			audiodcmap[msg.SeqID] = dc
			AudioDataChannelHandler(dc, mediatype)
		}

	})

	var offer webrtc.SessionDescription
	offer = msg.RtcSession
	//fmt.Println("offer", offer)
	if err = peerConnection.SetRemoteDescription(offer); err != nil {
		logger.Errorf("SetRemoteDescription error %v", err)
	}
	fmt.Printf("6")
	gatherCompletePromise := webrtc.GatheringCompletePromise(peerConnection)
	fmt.Printf("7")
	answer, err := peerConnection.CreateAnswer(nil)
	if err != nil {
		logger.Errorf("CreateAnswer error: %v", err)
	} else if err = peerConnection.SetLocalDescription(answer); err != nil {
		logger.Errorf("SetLocalDescription error: %v", err)
	}

	fmt.Println("wait gatherCompletePromise")
	<-gatherCompletePromise
	/*
	   response, err := json.Marshal(answer)
	   if err != nil {
	   	panic(err)
	   }
	*/
	req := &Session{}
	req.Type = "answer"
	req.DeviceId = config.Config.Mqtt.CLIENTID //"kvm1"
	req.Data = enc.Encode(*peerConnection.LocalDescription())
	//data := signal.Encode(*peerConnection.LocalDescription())
	answermsg := PublishMsg{
		WEB_SEQID: msg.SeqID,
		Topic:     "answer",
		Msg:       req,
	}
	// fmt.Println("answer %s", msg.SeqID)
	fmt.Println("answer %s", msg.SeqID)
	SendMsg(answermsg) //response)
}

H265 数据发送

func H265DataChannelHandler(dc *webrtc.DataChannel, mediatype string) {
	fmt.Printf("H265DataChannelHandler\n")

	// syschan := make(chan struct{}, 1)
	dc.OnOpen(func() {
		// syschan := make(chan struct{}, 1)
		nInSendH265Track++
		fmt.Printf("dc.OnOpen %d\n", nInSendH265Track)
		sendH265ImportFrame(dc, utils.NALU_H265_SEI)
		sendH265ImportFrame(dc, utils.NALU_H265_VPS)
		sendH265ImportFrame(dc, utils.NALU_H265_SPS)
		sendH265ImportFrame(dc, utils.NALU_H265_PPS)
		sendH265ImportFrame(dc, utils.NALU_H265_IFRAME)

		if nInSendH265Track <= 1 {

			go func() {
				fmt.Println("read stdin for h265\n")
				fmt.Println("start thread for  h265 ok\n")
				sig := make(chan os.Signal)
				signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL, syscall.SIGABRT, syscall.SIGQUIT)
				var file *os.File
				var err error
				if !USE_FILE_UPLOAD {
					rk1126.ResumeH264()
				} else {
					fileName := "./h265_high.mp4"
					file, err = os.Open(fileName)
					defer file.Close()
					if err != nil {
						fmt.Println("Open the file failed,err:", err)
						os.Exit(-1)
					}
					fmt.Println("open file ", fileName, " ok\n")

				}
				// // h264FrameDuration=
				timestart := time.Now().UnixMilli()
				ticker := time.NewTicker(h264FrameDuration)
				defer ticker.Stop()
				for {
					select {
					// case <-syschan:
					// 	rk1126.PauseH264()
					// 	fmt.Println("get h265 dc close signale")
					// 	break
					// case <-time.After(1 * time.Second):
					// 	fmt.Println("h264 timer")
					case <-sig:
						rk1126.PauseH264()
						break
					case <-sysvideochan:
						rk1126.PauseH264()
						fmt.Println("sysvideochan exit")
						nInSendH265Track = 0
						return
					case <-ticker.C:
						// default:
						if nInSendH265Track <= 0 {
							rk1126.PauseH264()
							fmt.Println("no dc channel exit")
							return
						}
						if USE_FILE_UPLOAD {
							var arr [256]byte
							var buf []byte
							bufflen := 0
							for {
								// var arr [MAXPACKETSIZE]byte

								n, err := file.Read(arr[:])
								if err == io.EOF {
									fmt.Println("file read finished")
									file.Seek(0, 0)
									continue
									//break
								}
								if err != nil {
									fmt.Println("file read failed", err)
									os.Exit(-1)
								}
								buf = append(buf, arr[:n]...)
								bufflen += n
								if bufflen >= MAXPACKETSIZE {
									break
								}
							}
							h265 := &rk1126.Mediadata{}
							h265.Data = buf
							h265.Len = bufflen
							timestart := time.Now().UnixMilli()
							for _, vdc := range H265dcmap {
								SendH265FrameData(vdc, h265, timestart)
							}
							time.Sleep(h264FrameDuration)
						} else {
							delayms := time.Now().UnixMilli() - timestart
							fmt.Println("send H265 delay ", delayms)
							// fmt.Println("GetH264Data start nInSendH264Track->", nInSendH264Track)
							timestart = time.Now().UnixMilli()
							h265 := rk1126.GetH264Data()
							// data := h264.Data[0 : h264.Len-1]
							if h265 != nil {
								for _, vdc := range H265dcmap {
									// fmt.Println("\r\nSendH265FrameData ", vdc)
									SendH265FrameData(vdc, h265, h265.Timestamp.Milliseconds())
								}
								rk1126.VideoDone(h265)
								// fmt.Println("\r\nh264 send ok")
							} else {
								fmt.Println("h265 data is nil")
							}
							// delayms := time.Now().UnixMilli() - timestart
							// fmt.Println("send H265 delay ", delayms)
						}

					}

				}
				fmt.Println("h265 thread exit")
				nInSendH265Track = 0
			}()
		}
	})
	dc.OnMessage(func(msg webrtc.DataChannelMessage) {
		msg_ := string(msg.Data)
		fmt.Println(msg_)

	})
	dc.OnClose(func() {
		fmt.Println("hd265 dc close")
		nInSendH265Track--
		for k, v := range H265dcmap {
			if v == dc {
				delete(H265dcmap, k)
			}
		}
		if mediatype == "audio" {
			nInSendAudioTrack--
			if nInSendAudioTrack <= 0 {
				//用户全部退出就是让采集程序退出
				fmt.Println("sysaudiochan 退出")
				if sysaudiochan != nil {
					sysaudiochan <- struct{}{}
				}
			}
			// for k, v := range audiodcmap {
			// 	if v == dc {
			// 		delete(audiodcmap, k)
			// 	}
			// }

		}
		// syschan <- struct{}{}
	})

}

 至此,我们就打通了浏览器和设备或者服务器端的通道,并根据信令参数实现取流发送

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值