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{}{}
})
}
至此,我们就打通了浏览器和设备或者服务器端的通道,并根据信令参数实现取流发送