浏览器播放rtsp视频流:3、rtsp转webrtc播放

2023-11-19

浏览器播放rtsp视频流:3、rtsp转webrtc播放


1. 前言

前面我们测试了rtsp转hls方式,发现延迟比较大,不太适合我们的使用需求。接下来我们试一下webrtc的方式看下使用情况。

综合考虑下来,我们最好能找到一个go作为后端,前端兼容性较好的前后端方案来处理webrtc,这样我们就可以结合我们之前的cgo+onvif+gSoap实现方案来获取rtsp流,并且可以根据已经实现的ptz、预置点等功能接口做更多的扩展。

2. rtsp转webRTC

如下是找到的一个比较合适的开源方案,前端使用了jQuery、bootstrap等,后端使用go+gin来实现并将rtsp流解析转换为webRTC协议提供http相关接口给到前端,通过config.json配置rtsp地址和stun地址:

https://github.com/deepch/RTSPtoWebRTC

此外,还带有stun,可以自行配置stun地址,便于进行内网穿透。

初步测试几乎看不出来延迟,符合预期,使用的jQuery+bootstrap+go+gin做的web,也符合我们的项目使用情况。

3. 初步测试结果

结果如下:

在这里插入图片描述

4. 结合我们之前的onvif+gSoap+cgo的方案做修改

我们在此项目的基础上,结合我们之前研究的onvif+cgo+gSoap的方案,将onvif获取到的相关数据提供接口到web端,增加ptz、调焦、缩放等功能。

我们在http.go中添加新的post接口:HTTPAPIServerStreamPtz来进行ptz和HTTPAPIServerStreamPreset进行预置点相关操作。

以下是部分代码,没有做太多的优化,也仅仅实现了ptz、调焦和缩放,算是打通了通路,具体项目需要可以再做优化。

4.1 go后端修改

增加了新的接口,并将之前cgo+onvif+gSoap的内容结合了进来,项目整体没有做更多的优化,只是为了演示,提供一个思路:

http.go(增加了两个post接口ptz和preset,采用cgo方式处理):

package main

/*
#cgo CFLAGS: -I ./ -I /usr/local/
#cgo LDFLAGS: -L ./build -lc_onvif_static -lpthread -ldl -lssl -lcrypto
#include "client.h"
#include "malloc.h"
*/
import "C"

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
    "sort"
    "strconv"
    "time"
    "unsafe"

    "github.com/deepch/vdk/av"

    webrtc "github.com/deepch/vdk/format/webrtcv3"
    "github.com/gin-gonic/gin"
)

type JCodec struct {
    Type string
}

func serveHTTP() {
    gin.SetMode(gin.ReleaseMode)

    router := gin.Default()
    router.Use(CORSMiddleware())

    if _, err := os.Stat("./web"); !os.IsNotExist(err) {
        router.LoadHTMLGlob("web/templates/*")
        router.GET("/", HTTPAPIServerIndex)
        router.GET("/stream/player/:uuid", HTTPAPIServerStreamPlayer)
    }
    router.POST("/stream/receiver/:uuid", HTTPAPIServerStreamWebRTC)
    //增加新的post接口
    router.POST("/stream/ptz/", HTTPAPIServerStreamPtz)
    router.POST("/stream/preset/", HTTPAPIServerStreamPreset)
    router.GET("/stream/codec/:uuid", HTTPAPIServerStreamCodec)
    router.POST("/stream", HTTPAPIServerStreamWebRTC2)

    router.StaticFS("/static", http.Dir("web/static"))
    err := router.Run(Config.Server.HTTPPort)
    if err != nil {
        log.Fatalln("Start HTTP Server error", err)
    }
}

//HTTPAPIServerIndex  index
func HTTPAPIServerIndex(c *gin.Context) {
    _, all := Config.list()
    if len(all) > 0 {
        c.Header("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store")
        c.Header("Access-Control-Allow-Origin", "*")
        c.Redirect(http.StatusMovedPermanently, "stream/player/"+all[0])
    } else {
        c.HTML(http.StatusOK, "index.tmpl", gin.H{
            "port":    Config.Server.HTTPPort,
            "version": time.Now().String(),
        })
    }
}

//HTTPAPIServerStreamPlayer stream player
func HTTPAPIServerStreamPlayer(c *gin.Context) {
    _, all := Config.list()
    sort.Strings(all)
    c.HTML(http.StatusOK, "player.tmpl", gin.H{
        "port":     Config.Server.HTTPPort,
        "suuid":    c.Param("uuid"),
        "suuidMap": all,
        "version":  time.Now().String(),
    })
}

//HTTPAPIServerStreamCodec stream codec
func HTTPAPIServerStreamCodec(c *gin.Context) {
    if Config.ext(c.Param("uuid")) {
        Config.RunIFNotRun(c.Param("uuid"))
        codecs := Config.coGe(c.Param("uuid"))
        if codecs == nil {
            return
        }
        var tmpCodec []JCodec
        for _, codec := range codecs {
            if codec.Type() != av.H264 && codec.Type() != av.PCM_ALAW && codec.Type() != av.PCM_MULAW && codec.Type() != av.OPUS {
                log.Println("Codec Not Supported WebRTC ignore this track", codec.Type())
                continue
            }
            if codec.Type().IsVideo() {
                tmpCodec = append(tmpCodec, JCodec{Type: "video"})
            } else {
                tmpCodec = append(tmpCodec, JCodec{Type: "audio"})
            }
        }
        b, err := json.Marshal(tmpCodec)
        if err == nil {
			_, err = c.Writer.Write(b)
			if err != nil {
				log.Println("Write Codec Info error", err)
				return
			}
		}
	}
}

//HTTPAPIServerStreamWebRTC stream video over WebRTC
func HTTPAPIServerStreamWebRTC(c *gin.Context) {
	if !Config.ext(c.PostForm("suuid")) {
		log.Println("Stream Not Found")
		return
	}
	Config.RunIFNotRun(c.PostForm("suuid"))
	codecs := Config.coGe(c.PostForm("suuid"))
	if codecs == nil {
		log.Println("Stream Codec Not Found")
		return
	}
	var AudioOnly bool
	if len(codecs) == 1 && codecs[0].Type().IsAudio() {
		AudioOnly = true
	}
	muxerWebRTC := webrtc.NewMuxer(webrtc.Options{ICEServers: Config.GetICEServers(), ICEUsername: Config.GetICEUsername(), ICECredential: Config.GetICECredential(), PortMin: Config.GetWebRTCPortMin(), PortMax: Config.GetWebRTCPortMax()})
	answer, err := muxerWebRTC.WriteHeader(codecs, c.PostForm("data"))
	if err != nil {
		log.Println("WriteHeader", err)
		return
	}
	_, err = c.Writer.Write([]byte(answer))
	if err != nil {
		log.Println("Write", err)
		return
	}
	go func() {
		cid, ch := Config.clAd(c.PostForm("suuid"))
		defer Config.clDe(c.PostForm("suuid"), cid)
		defer muxerWebRTC.Close()
		var videoStart bool
		noVideo := time.NewTimer(10 * time.Second)
		for {
			select {
			case <-noVideo.C:
				log.Println("noVideo")
				return
			case pck := <-ch:
				if pck.IsKeyFrame || AudioOnly {
					noVideo.Reset(10 * time.Second)
					videoStart = true
				}
				if !videoStart && !AudioOnly {
					continue
				}
				err = muxerWebRTC.WritePacket(pck)
				if err != nil {
					log.Println("WritePacket", err)
					return
				}
			}
		}
	}()
}

func HTTPAPIServerStreamPtz(c *gin.Context) {
	action := c.PostForm("action")
	direction, err := strconv.Atoi(action)
	if err != nil {
		log.Println(err)
		return
	}
	var soap C.P_Soap
	soap = C.new_soap(soap)
	username := C.CString("admin")
	password := C.CString("admin")
	serviceAddr := C.CString("http://40.40.40.101:80/onvif/device_service")

	C.get_device_info(soap, username, password, serviceAddr)

	mediaAddr := [200]C.char{}
	C.get_capabilities(soap, username, password, serviceAddr, &mediaAddr[0])

	profileToken := [200]C.char{}
	C.get_profiles(soap, username, password, &profileToken[0], &mediaAddr[0])

	videoSourceToken := [200]C.char{}
	C.get_video_source(soap, username, password, &videoSourceToken[0], &mediaAddr[0])

	switch direction {
	case 0:
		break
	case 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11:
		C.ptz(soap, username, password, C.int(direction), C.float(0.5), &profileToken[0], &mediaAddr[0])
	case 12, 13, 14:
		C.focus(soap, username, password, C.int(direction), C.float(0.5), &videoSourceToken[0], &mediaAddr[0])
	default:
		fmt.Println("Unknown direction.")
	}
	C.del_soap(soap)

	C.free(unsafe.Pointer(username))
	C.free(unsafe.Pointer(password))
	C.free(unsafe.Pointer(serviceAddr))

	c.JSON(http.StatusOK, gin.H{"message":"success"})
}

func HTTPAPIServerStreamPreset(c *gin.Context) {
	var soap C.P_Soap
	soap = C.new_soap(soap)
	username := C.CString("admin")
	password := C.CString("admin")
	serviceAddr := C.CString("http://40.40.40.101:80/onvif/device_service")

	C.get_device_info(soap, username, password, serviceAddr)

	mediaAddr := [200]C.char{}
	C.get_capabilities(soap, username, password, serviceAddr, &mediaAddr[0])

	profileToken := [200]C.char{}
	C.get_profiles(soap, username, password, &profileToken[0], &mediaAddr[0])

	videoSourceToken := [200]C.char{}
	C.get_video_source(soap, username, password, &videoSourceToken[0], &mediaAddr[0])

	action := c.PostForm("action")
	presetAction, err := strconv.Atoi(action)
	if err != nil {
		log.Println(err)
		return
	}
	fmt.Println("请输入数字进行preset,1-4分别代表查询、设置、跳转、删除预置点;退出输入0:")
	switch presetAction {
	case 0:
		break
	case 1:
		C.preset(soap, username, password, C.int(presetAction), nil, nil, &profileToken[0], &mediaAddr[0])
	case 2:
		fmt.Println("请输入要设置的预置点token信息:")
		presentToken := ""
		_, _ = fmt.Scanln(&presentToken)
		fmt.Println("请输入要设置的预置点name信息长度不超过200:")
		presentName := ""
		_, _ = fmt.Scanln(&presentName)
		C.preset(soap, username, password, C.int(presetAction), C.CString(presentToken), C.CString(presentName), &profileToken[0], &mediaAddr[0])
	case 3:
		fmt.Println("请输入要跳转的预置点token信息:")
		presentToken := ""
		_, _ = fmt.Scanln(&presentToken)
		C.preset(soap, username, password, C.int(presetAction), C.CString(presentToken), nil, &profileToken[0], &mediaAddr[0])
	case 4:
		fmt.Println("请输入要删除的预置点token信息:")
		presentToken := ""
		_, _ = fmt.Scanln(&presentToken)
		C.preset(soap, username, password, C.int(presetAction), C.CString(presentToken), nil, &profileToken[0], &mediaAddr[0])
	default:
		fmt.Println("unknown present action.")
		break
	}

	C.del_soap(soap)

	C.free(unsafe.Pointer(username))
	C.free(unsafe.Pointer(password))
	C.free(unsafe.Pointer(serviceAddr))
	
	c.JSON(http.StatusOK, gin.H{"message":"success"})
}

func CORSMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Header("Access-Control-Allow-Origin", "*")
		c.Header("Access-Control-Allow-Credentials", "true")
		c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, x-access-token")
		c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
		c.Header("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")

		if c.Request.Method == "OPTIONS" {
			c.AbortWithStatus(http.StatusNoContent)
			return
		}

		c.Next()
	}
}

type Response struct {
	Tracks []string `json:"tracks"`
	Sdp64  string   `json:"sdp64"`
}

type ResponseError struct {
	Error string `json:"error"`
}

func HTTPAPIServerStreamWebRTC2(c *gin.Context) {
	url := c.PostForm("url")
	if _, ok := Config.Streams[url]; !ok {
		Config.Streams[url] = StreamST{
			URL:      url,
			OnDemand: true,
			Cl:       make(map[string]viewer),
		}
	}

	Config.RunIFNotRun(url)

	codecs := Config.coGe(url)
	if codecs == nil {
		log.Println("Stream Codec Not Found")
		c.JSON(500, ResponseError{Error: Config.LastError.Error()})
		return
	}

	muxerWebRTC := webrtc.NewMuxer(
		webrtc.Options{
			ICEServers: Config.GetICEServers(),
			PortMin:    Config.GetWebRTCPortMin(),
			PortMax:    Config.GetWebRTCPortMax(),
		},
	)

	sdp64 := c.PostForm("sdp64")
	answer, err := muxerWebRTC.WriteHeader(codecs, sdp64)
	if err != nil {
		log.Println("Muxer WriteHeader", err)
		c.JSON(500, ResponseError{Error: err.Error()})
		return
	}

	response := Response{
		Sdp64: answer,
	}

	for _, codec := range codecs {
		if codec.Type() != av.H264 &&
			codec.Type() != av.PCM_ALAW &&
			codec.Type() != av.PCM_MULAW &&
			codec.Type() != av.OPUS {
			log.Println("Codec Not Supported WebRTC ignore this track", codec.Type())
			continue
		}
		if codec.Type().IsVideo() {
			response.Tracks = append(response.Tracks, "video")
		} else {
			response.Tracks = append(response.Tracks, "audio")
		}
	}

	c.JSON(200, response)

	AudioOnly := len(codecs) == 1 && codecs[0].Type().IsAudio()

	go func() {
		cid, ch := Config.clAd(url)
		defer Config.clDe(url, cid)
		defer muxerWebRTC.Close()
		var videoStart bool
		noVideo := time.NewTimer(10 * time.Second)
		for {
			select {
			case <-noVideo.C:
				log.Println("noVideo")
				return
			case pck := <-ch:
				if pck.IsKeyFrame || AudioOnly {
					noVideo.Reset(10 * time.Second)
					videoStart = true
				}
				if !videoStart && !AudioOnly {
					continue
				}
				err = muxerWebRTC.WritePacket(pck)
				if err != nil {
					log.Println("WritePacket", err)
					return
				}
			}
		}
	}()
}

4.2 前端修改

对于goland我们首先将.tmpl文件通过右键标记为html格式,然后再修改时就会有前端语法支持和补全支持,便于修改,否则默认是识别为文本的,之后我们修改player.tmpl和app.js,在player.tmpl中添加一些ptz的按钮并通过js与前后端进行数据交互:

player.tmpl:

<html>
<meta http-equiv="Expires" content="0">
<meta http-equiv="Last-Modified" content="0">
<meta http-equiv="Cache-Control" content="no-cache, mustrevalidate">
<meta http-equiv="Pragma" content="no-cache">
<link rel="stylesheet" href="../../static/css/bootstrap.min.css">
<link rel="stylesheet" href="../../static/css/shanxing.css">
<script type="text/javascript" src="../../static/js/jquery-3.4.1.min.js"></script>
<script src="../../static/js/bootstrap.min.js"></script>
<script src="../../static/js/adapter-latest.js"></script>

<h2 align=center>
    Play Stream {{ .suuid }}<br>
</h2>
<div class="container">
    <div class="row">
        <div class="col-3">
            <div class="list-group">
                {{ range .suuidMap }}
                <a href="{{ . }}" id="{{ . }}" name="{{ . }}" class="list-group-item list-group-item-action">{{ . }}</a>
                {{ end }}
                </br>
                <div class="sector">
                    <div class="box s1" id="top" onclick="funTopClick()">
                    </div>
                    <div class="box s2" id="right" onclick="funRightClick()">
                    </div>
                    <div class="box s3" id="down" onclick="funDownClick()">
                    </div>
                    <div class="box s4" id="left" onclick="funLeftClick()">
                    </div>
                    <div class="center" id="stop" onclick="funStopClick()">
                    </div>
                </div>
                <div class="btn-group">
                    <button type="button" class="btn btn-default" onclick="funZoomClick(10)">缩放+</button>
                    <button type="button" class="btn btn-default" onclick="funZoomClick(11)">缩放-</button>
                </div>
                <div class="btn-group">
                    <button type="button" class="btn btn-default" onclick="funFocusClick(12)">调焦+</button>
                    <button type="button" class="btn btn-default" onclick="funFocusClick(13)">调焦-</button>
                    <button type="button" class="btn btn-default" onclick="funFocusClick(14)">停止调焦</button>
                </div>
            </div>
        </div>
        <div class="col">
            <input type="hidden" name="suuid" id="suuid" value="{{ .suuid }}">
            <input type="hidden" name="port" id="port" value="{{ .port }}">
            <input type="hidden" id="localSessionDescription" readonly="true">
            <input type="hidden" id="remoteSessionDescription">
            <div id="remoteVideos">
                <video style="width:600px" id="videoElem" autoplay muted controls></video>
            </div>
            <div id="div"></div>
        </div>
    </div>
</div>
<script type="text/javascript" src="../../static/js/app.js?ver={{ .version }}"></script>
</html>

app.js:

let stream = new MediaStream();
let suuid = $('#suuid').val();

let config = {
  iceServers: [{
    urls: ["stun:stun.l.google.com:19302"]
  }]
};

const pc = new RTCPeerConnection(config);
pc.onnegotiationneeded = handleNegotiationNeededEvent;

let log = msg => {
  document.getElementById('div').innerHTML += msg + '<br>'
}

pc.ontrack = function(event) {
  stream.addTrack(event.track);
  videoElem.srcObject = stream;
  log(event.streams.length + ' track is delivered')
}

pc.oniceconnectionstatechange = e => log(pc.iceConnectionState)

async function handleNegotiationNeededEvent() {
  let offer = await pc.createOffer();
  await pc.setLocalDescription(offer);
  getRemoteSdp();
}

$(document).ready(function() {
  $('#' + suuid).addClass('active');
  getCodecInfo();
});


function getCodecInfo() {
  $.get("../codec/" + suuid, function(data) {
    try {
      data = JSON.parse(data);
    } catch (e) {
      console.log(e);
    } finally {
      $.each(data,function(index,value){
        pc.addTransceiver(value.Type, {
          'direction': 'sendrecv'
        })
      })
    }
  });
}

let sendChannel = null;

function getRemoteSdp() {
  $.post("../receiver/"+ suuid, {
    suuid: suuid,
    data: btoa(pc.localDescription.sdp)
  }, function(data) {
    try {
      pc.setRemoteDescription(new RTCSessionDescription({
        type: 'answer',
        sdp: atob(data)
      }))
    } catch (e) {
      console.warn(e);
    }
  });
}

function ptz(direction) {
  $.post("../ptz/", direction, function(data, status){
    console.debug("Data: " + data + "nStatus: " + status);
  });
}

function funTopClick() {
  console.debug("top click");
  ptz("action=1")
}

function funDownClick() {
  console.debug("down click");
  ptz("action=2")
}

function funLeftClick() {
  console.debug("left click");
  ptz("action=3")
}

function funRightClick() {
  console.debug("right click");
  ptz("action=4")
}

function funStopClick() {
  console.debug("stop click");
  ptz("action=9")
}

function funZoomClick(direction) {
  console.debug("zoom click"+direction);
  ptz("action="+direction)
}

function funFocusClick(direction) {
  console.debug("focus click"+direction);
  ptz("action="+direction)
}

主要增加了一个扇形按钮和两组按钮组,然后将按钮的点击结合到app.js中进行处理,app.js中则发送post请求调用go后端接口。

4.3 项目结构和编译运行

项目结构如下,部分文件做了备份,实际可以不用:

$tree -a -I ".github|.idea|
build"
.
├── .gitignore
├── CMakeLists.txt
├── Dockerfile
├── LICENSE
├── README.md
├── build.cmd
├── client.c
├── client.h
├── config.go
├── config.json
├── config.json.bak
├── doc
│   ├── demo2.png
│   ├── demo3.png
│   └── demo4.png
├── go.mod
├── go.sum
├── http.go
├── main.go
├── main.go.bak
├── renovate.json
├── soap
│   ├── DeviceBinding.nsmap
│   ├── ImagingBinding.nsmap
│   ├── MediaBinding.nsmap
│   ├── PTZBinding.nsmap
│   ├── PullPointSubscriptionBinding.nsmap
│   ├── RemoteDiscoveryBinding.nsmap
│   ├── custom
│   │   ├── README.txt
│   │   ├── chrono_duration.cpp
│   │   ├── chrono_duration.h
│   │   ├── chrono_time_point.cpp
│   │   ├── chrono_time_point.h
│   │   ├── duration.c
│   │   ├── duration.h
│   │   ├── float128.c
│   │   ├── float128.h
│   │   ├── int128.c
│   │   ├── int128.h
│   │   ├── long_double.c
│   │   ├── long_double.h
│   │   ├── long_time.c
│   │   ├── long_time.h
│   │   ├── qbytearray_base64.cpp
│   │   ├── qbytearray_base64.h
│   │   ├── qbytearray_hex.cpp
│   │   ├── qbytearray_hex.h
│   │   ├── qdate.cpp
│   │   ├── qdate.h
│   │   ├── qdatetime.cpp
│   │   ├── qdatetime.h
│   │   ├── qstring.cpp
│   │   ├── qstring.h
│   │   ├── qtime.cpp
│   │   ├── qtime.h
│   │   ├── struct_timeval.c
│   │   ├── struct_timeval.h
│   │   ├── struct_tm.c
│   │   ├── struct_tm.h
│   │   ├── struct_tm_date.c
│   │   └── struct_tm_date.h
│   ├── dom.c
│   ├── dom.h
│   ├── duration.c
│   ├── duration.h
│   ├── mecevp.c
│   ├── mecevp.h
│   ├── onvif.h
│   ├── smdevp.c
│   ├── smdevp.h
│   ├── soapC.c
│   ├── soapClient.c
│   ├── soapH.h
│   ├── soapStub.h
│   ├── stdsoap2.h
│   ├── stdsoap2_ssl.c
│   ├── struct_timeval.c
│   ├── struct_timeval.h
│   ├── threads.c
│   ├── threads.h
│   ├── typemap.dat
│   ├── wsaapi.c
│   ├── wsaapi.h
│   ├── wsdd.nsmap
│   ├── wsseapi.c
│   └── wsseapi.h
├── stream.go
└── web
    ├── static
    │   ├── css
    │   │   ├── bootstrap-grid.css
    │   │   ├── bootstrap-grid.css.map
    │   │   ├── bootstrap-grid.min.css
    │   │   ├── bootstrap-grid.min.css.map
    │   │   ├── bootstrap-reboot.css
    │   │   ├── bootstrap-reboot.css.map
    │   │   ├── bootstrap-reboot.min.css
    │   │   ├── bootstrap-reboot.min.css.map
    │   │   ├── bootstrap.css
    │   │   ├── bootstrap.css.map
    │   │   ├── bootstrap.min.css
    │   │   ├── bootstrap.min.css.map
    │   │   └── shanxing.css
    │   └── js
    │       ├── adapter-latest.js
    │       ├── app.js
    │       ├── bootstrap.bundle.js
    │       ├── bootstrap.bundle.js.map
    │       ├── bootstrap.bundle.min.js
    │       ├── bootstrap.bundle.min.js.map
    │       ├── bootstrap.js
    │       ├── bootstrap.js.map
    │       ├── bootstrap.min.js
    │       ├── bootstrap.min.js.map
    │       └── jquery-3.4.1.min.js
    └── templates
        ├── index.tmpl
        └── player.tmpl

8 directories, 111 files

关于cgo和onvif、gSoap部分这里就不多说了,不清楚的可以看前面的总结,gin、bootstramp、jQuery这些也需要一定的前后端概念学习和储备,在其它的分类总结中也零星分布了,不清楚的可以看一下,这里就不再多说了。

编译运行:

GOOS=linux GOARCH=amd64 CGO_ENABLE=1 GO111MODULE=on go run *.go

记得修改一下go.mod中对go版本的依赖,按照cgo的问题,目前至少需要1.18及以上,否则运行ptz可能出现分割违例问题,到我总结这里1.18已经发了正式版本了。

module github.com/deepch/RTSPtoWebRTC

go 1.18

require (
	github.com/deepch/vdk v0.0.0-20220309163430-c6529706436c
	github.com/gin-gonic/gin v1.7.7
)

4.4 结果展示

界面效果:

在这里插入图片描述

动态调试ptz:

在这里插入图片描述

动态调试缩放:

在这里插入图片描述

动态调试调焦:

在这里插入图片描述

5. 最后

webRTC使用起来几乎感觉不到延迟,但是受制于stun的udp打洞的稳定性,可能会出现卡顿掉线等情况,所以还牵扯到p2p的问题,需要注意这一点,当然,这是远程推流都绕不开的一点,也不算是独有的问题。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

浏览器播放rtsp视频流:3、rtsp转webrtc播放 的相关文章

  • Go 中的格式错误 - %s %v 或 %w

    s v and w可用于格式化 Go 中的错误 将它们转换为字符串 fmt Errorf 它们在 Go 自己的工具中的使用方式似乎有所不同 In cmd go internal get path go https github com go
  • 如何更新任意go结构的所有字符串字段?

    我尝试编写一个函数来更新所有字符串字段随意的结构体 像这样 type Student struct Name string Age int func SetStringField obj interface reflect ValueOf
  • 从头开始使用映像部署无法启动

    我正在使用以下内容构建图像Dockerfile FROM golang 1 19 2 bullseye as builder COPY src src WORKDIR src RUN CGO ENABLED 1 go build race
  • gcloud 部署应用程序找不到导入包 - golang

    我已经将应用程序的一个版本部署到 GAE 但现在部署新版本时遇到问题 当我尝试时gcloud app deploy version VERSION 我收到一堆错误 显示远程构建找不到我的导入包 Beginning deployment of
  • 如何分发仅二进制的 go 包

    我想以二进制形式分发包而不包含源代码 我的演示项目目录结构是这样的 demo greet greet go hi hi go hello hello go main go main go package main import fmt de
  • 使用 Golang 通道处理 HTTP 请求

    我正在尝试构建一个简单的 Golang Appengine 应用程序 它使用通道来处理每个 http 请求 原因是我希望每个请求执行合理的大型内存计算 并且每个请求都以线程安全的方式执行 即来自并发请求的计算不会混合 这一点很重要 本质上
  • golang:使用 gin 路由器服务 net.Conn

    我有一个处理传入 TCP 连接的函数 func Handle conn net Conn error 另外 我有一个初始化的 gin 路由器 带有已实现的句柄 router gin New router GET router POST Th
  • 为什么结构体不能转换为嵌入类型

    package main type Inner struct x int type Outer struct Inner func main x Inner 1 y Outer x cannot convert x type Inner t
  • 在 Go 中修改导入的库

    我的问题 弹性节拍 https www elastic co products beats是一个用 Go 编写的日志传送程序的开源项目 它具有多种日志输出功能 包括控制台 Elasticsearch 和 Redis 我想将我自己的输出添加到
  • Cgo 生成的源无法在 MVC 上编译

    我有一个用 CGo 制作的共享库 它在 Linux 和 Android 上链接得很好 但是 当使用 Microsoft Visual Studio 2017 在 Windows 10 上进行编译时 出现以下错误 Microsoft R Pr
  • Go 中数组的嵌套结构

    我已经开始使用https mholt github io json to go https mholt github io json to go 将 API JSON 转换为 go 结构 我真的很喜欢它 但是我对如何初始化如下所示的报告定义
  • 如何通过模板中的变量访问对象字段?

    我有一个嵌套循环 columns columns range dx dataList range c columns index dx c end end dataList是orm模型数组 和ID Title字段 那么columns is
  • golang从sdin扫描一行数字

    我正在尝试从标准输入读取输入 3 2 1
  • 在 Go 中执行字节数组

    我正在尝试在 Go 程序中执行 shellcode 类似于使用其他语言执行此操作的方式 示例 1 C 程序中的 Shellcode https stackoverflow com questions 16626857 shellcode i
  • 在 Go 中生成随机、固定长度的字节数组

    我有一个字节数组 固定长度为4 token make byte 4 我需要将每个字节设置为随机字节 我怎样才能以最有效的方式做到这一点 这math rand就我而言 方法不提供随机字节函数 也许有一种内置的方法 或者我应该生成一个随机字符串
  • 为什么结构中“[0]byte”的位置很重要?

    0 byte在golang中不应该占用任何内存空间 但这两个结构体的大小不同 type bar2 struct A int 0 byte type bar3 struct 0 byte A int 那么为什么这个位置 0 byte这里重要吗
  • Go io.Pipe 的缓冲版本

    有缓冲版本吗io Pipe https golang org pkg io Pipe 在标准库或第三方库中 在我推出自己的库之前 上下文 我正在尝试使用这个解决方案 https stackoverflow com a 36229262 15
  • GoQt 致命错误:QAbstractAnimation:没有这样的文件或目录

    我尝试编译 Qt 来开发桌面应用程序 我按照 Qt 网站上的官方 wiki 指南的说明进行操作 当我尝试go run示例文件夹中的示例 我收到错误 去运行 home pinkya rabbit workspace go1programs s
  • GOPATH值设置

    我用go1 3 1 windows amd64 msi安装go 安装后GOROOT是默认设置 我发现 D Programs Go bin 在 PATH 中 然后我创建一个 GOPATH 环境变量 使用 go get 命令时 出现错误 软件包
  • 有没有办法在 VSCode 中保存时运行 go 测试,并将其输出到终端?

    现在我有几个项目在VSCode中运行 运行起来相当繁琐go test每次我编写新代码时 我宁愿立即看看我是否破坏了某些东西 我知道在 Javascript 中我可以在每次保存文件时运行测试 并将输出发送到终端 现在我正在使用 保存时运行 h

随机推荐

  • django高并发部署

    django高并发部署
  • React解密:React Hooks函数之useCallback和useMemo

    之所以将useCallback和useMemo放到一起 从某种意义上说 他们都是性能优化的始作俑者 他们也有很多的共性 我们先来回顾一下class组件性能优化的点 调用 setState 就会触发组件的重新渲染 无论前后 state 是否相
  • 64位系统树莓派部署yolo-fatestv2---超多坑

    最近在研究yolo fastest 开始面对作者大大的一堆部署的指令在pycharm的终端里面一顿操作 然后一路报错 后来才发现原来都是linux的指令 后来在虚拟机上也尝试部署过 成功之后本来想直接挪到树莓派上 但是尝试许久都以失败告终
  • mysql修改权限

    mysql权限 显示用户权限 mysql gt show grants for jeffrey localhost 一般 数据库管理员首先建立用户 定义其非特权特征 例如其密码 是否使用安全链接以及对服务器资源的访问限制 而后使用grant
  • java枚举类的定义和使用

    开始时间 2018年8月11日11 04 35 结束时间 2018年8月11日12 30 37 累计 1小时 枚举类的对象是有限个 对象个数 一个可以看做单例模式的实现 多个 为枚举类 1 如何定义 cccccccc 1 私有化类的构造器
  • 服务器虚拟化解决方案

    根据以往经验推断 一台主流双路 PC 服务器可以承担 3 6 个应用系统在其 上运行 本期项目总共有 N 个业务系统 考虑到硬件资源需具备一定的冗余能 力和实现高可用 HA 在线迁移 动态调度 后期扩展等诸多因素 推荐 2 台双 路 PC
  • 智能指针 -- unique_ptr

    源码分析 源码链接 gcc unique ptr h at master gcc mirror gcc GitHub 上面链接中的源码是unique ptr的完整定义 我们来简化其类结构看看 template
  • Android基础面试常常死在这几个问题上,小白也能看明白

    前言 疫情一过 我相信将会是面试求职的高峰时期 如果此时手里有份高质量的面试宝典 那么你将得心应手面对考官各种问题 虽然不敢保证你能应聘上心仪的职位 但是能保证看完这些内容你的收获将超乎你的想象 此份面试宝典搜集各大网络平台 如果侵权 请您
  • 【黑叔说】之《进阶必备知识》(一)

    前言 每天一分钟 通勤跟我学 进阶知识点 系列的知识 来自互联网 由黑叔总结或改编 仅供参考 一 前端模块化 二 webpack简易版实现 function modules function require fileName const f
  • 单片机流水灯C语言实验报告,单片机LED灯实验报告.doc

    桂林电子科技大学 实验报告 2016 2017 学年第一学期 开 课 单 位 海洋信息工程学院 适用年级 专业 14级机械 课 程 序 号 BS1615000 03 课 程 代 码 BS1615000 实 验 名 称 流水灯 流水灯实验报告
  • 6种方法计算神经网络参数量Params、计算量FLOPs、Macs简单代码

    方法1 统计模型参数量 total sum param nelement for param in model parameters print Number of parameter 2fM total 1e6 方法2 统计flops和参
  • Python——报数出圈

    编写程序 模拟报数游戏 有n个人围成一圈 顺序编号 从第一个人开始从1到k 假设k 3 报数 报到k的人退出圈子 然后圈子缩小 从下一个人继续游戏 问最后留下的是原来的第几号 n int input 请输入总人数 n k int input
  • 解决JavaScript中new Date(string)在IE不兼容的问题

    1 问题描述 let date 2018 09 17 let dateStr new Date date let year dateStr getFullYear let month dateStr getMonth 1 let day d
  • 【Unity2d】带你制作一款类似于金山打字的小游戏

    博主大概08年开始接触电脑游戏 当时玩的是我哥的电脑 那时候家里没网 只可以玩电脑上自带的单机游戏 比如扫雷 蜘蛛纸牌等等 当然还有红色警戒 冰封王座 星际争霸 帝国崛起等等 这些大概是我哥当时在大学下载的 也是那个时候对游戏充满了兴趣 记
  • 第十一届蓝桥杯c/c++省赛大学B组(第一次)

    目录 A题 跑步训练 5 模拟 B题 纪念日 5 年月日 C题 合并检测 10 找规律 D题 REPEAT程序 10 模拟 E题 矩阵 15 DP F题 整数序列 15 G题 解码 20 模拟 H题 走方格 20 dfs I题 整数拼接 2
  • 世界两万英尺范围内,均分布有运维体系架构

    几年前 Microsoft 与技术领先的社区专家合作发布了一本受欢迎的指导书 标题为 适用于容器化 NET 应用程序的 NET 微服务 深入探讨了构建分散式应用程序的原则 模式和最佳做法 其中包括一个功能齐全的微服务参考应用程序 展示了体系
  • [从零开始学DeepFaceLab-5]: 使用-命令行八大操作步骤-第2步:从源视频中提取图片

    目录 总体流程 步骤2 从源视频中提取图片 2 0 源视频文件和大小的选择 2 1 命令 2 extract images from video data src bat 必选
  • Qt

    我的RFID程序中 Widget继承自QWidget 在Widget h中 public定义了数据成员 QTableWidget cardtableWidget 在Widget cpp的Widget构造函数中 初始化了QTableWidge
  • 简明YAML教程

    前言 yaml是一种用来描述配置的语言 其可读性和简洁性较json更胜一筹 用yml写成的配置文件 以 yml结尾 YAML的基本语法规则 大小写敏感 使用缩进表示层级关系 缩进是使用空格 不允许使用tab 缩进对空格数目不敏感 相同层级需
  • 浏览器播放rtsp视频流:3、rtsp转webrtc播放

    浏览器播放rtsp视频流 3 rtsp转webrtc播放 文章目录 浏览器播放rtsp视频流 3 rtsp转webrtc播放 1 前言 2 rtsp转webRTC 3 初步测试结果 4 结合我们之前的onvif gSoap cgo的方案做修