电脑需要有摄像头
main.go
package main
import (
"encoding/json"
"flag"
"github.com/pion/webrtc/v4"
"log"
"net/http"
"os"
"sync"
"text/template"
"time"
"github.com/gorilla/websocket"
"github.com/pion/rtcp"
)
var (
addr = flag.String("addr", ":8080", "http service address")
upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
indexTemplate = &template.Template{}
listLock sync.RWMutex
peerConnections []peerConnectionState
trackLocals map[string]*webrtc.TrackLocalStaticRTP
)
type websocketMessage struct {
Event string `json:"event"`
Data string `json:"data"`
}
type peerConnectionState struct {
peerConnection *webrtc.PeerConnection
websocket *threadSafeWriter
}
func main() {
flag.Parse()
log.SetFlags(0)
trackLocals = map[string]*webrtc.TrackLocalStaticRTP{}
indexHTML, err := os.ReadFile("index.html")
if err != nil {
panic(err)
}
indexTemplate = template.Must(template.New("").Parse(string(indexHTML)))
http.HandleFunc("/websocket", websocketHandler)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if err := indexTemplate.Execute(w, "ws://"+r.Host+"/websocket"); err != nil {
log.Fatal(err)
}
})
go func() {
for range time.NewTicker(time.Second * 3).C {
dispatchKeyFrame()
}
}()
log.Fatal(http.ListenAndServe(*addr, nil))
}
func addTrack(t *webrtc.TrackRemote) *webrtc.TrackLocalStaticRTP {
listLock.Lock()
defer func() {
listLock.Unlock()
signalPeerConnections()
}()
trackLocal, err := webrtc.NewTrackLocalStaticRTP(t.Codec().RTPCodecCapability, t.ID(), t.StreamID())
if err != nil {
panic(err)
}
trackLocals[t.ID()] = trackLocal
return trackLocal
}
func removeTrack(t *webrtc.TrackLocalStaticRTP) {
listLock.Lock()
defer func() {
listLock.Unlock()
signalPeerConnections()
}()
delete(trackLocals, t.ID())
}
func signalPeerConnections() {
listLock.Lock()
defer func() {
listLock.Unlock()
dispatchKeyFrame()
}()
attemptSync := func() (tryAgain bool) {
for i := range peerConnections {
if peerConnections[i].peerConnection.ConnectionState() == webrtc.PeerConnectionStateClosed {
peerConnections = append(peerConnections[:i], peerConnections[i+1:]...)
return true
}
existingSenders := map[string]bool{}
for _, sender := range peerConnections[i].peerConnection.GetSenders() {
if sender.Track() == nil {
continue
}
existingSenders[sender.Track().ID()] = true
if _, ok := trackLocals[sender.Track().ID()]; !ok {
if err := peerConnections[i].peerConnection.RemoveTrack(sender); err != nil {
return true
}
}
}
for _, receiver := range peerConnections[i].peerConnection.GetReceivers() {
if receiver.Track() == nil {
continue
}
existingSenders[receiver.Track().ID()] = true
}
for trackID := range trackLocals {
if _, ok := existingSenders[trackID]; !ok {
if _, err := peerConnections[i].peerConnection.AddTrack(trackLocals[trackID]); err != nil {
return true
}
}
}
offer, err := peerConnections[i].peerConnection.CreateOffer(nil)
if err != nil {
return true
}
if err = peerConnections[i].peerConnection.SetLocalDescription(offer); err != nil {
return true
}
offerString, err := json.Marshal(offer)
if err != nil {
return true
}
if err = peerConnections[i].websocket.WriteJSON(&websocketMessage{
Event: "offer",
Data: string(offerString),
}); err != nil {
return true
}
}
return
}
for syncAttempt := 0; ; syncAttempt++ {
if syncAttempt == 25 {
go func() {
time.Sleep(time.Second * 3)
signalPeerConnections()
}()
return
}
if !attemptSync() {
break
}
}
}
func dispatchKeyFrame() {
listLock.Lock()
defer listLock.Unlock()
for i := range peerConnections {
for _, receiver := range peerConnections[i].peerConnection.GetReceivers() {
if receiver.Track() == nil {
continue
}
_ = peerConnections[i].peerConnection.WriteRTCP([]rtcp.Packet{
&rtcp.PictureLossIndication{
MediaSSRC: uint32(receiver.Track().SSRC()),
},
})
}
}
}
func websocketHandler(w http.ResponseWriter, r *http.Request) {
unsafeConn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Print("upgrade:", err)
return
}
c := &threadSafeWriter{unsafeConn, sync.Mutex{}}
defer c.Close()
peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{})
if err != nil {
log.Print(err)
return
}
defer peerConnection.Close()
for _, typ := range []webrtc.RTPCodecType{webrtc.RTPCodecTypeVideo, webrtc.RTPCodecTypeAudio} {
if _, err := peerConnection.AddTransceiverFromKind(typ, webrtc.RTPTransceiverInit{
Direction: webrtc.RTPTransceiverDirectionRecvonly,
}); err != nil {
log.Print(err)
return
}
}
listLock.Lock()
peerConnections = append(peerConnections, peerConnectionState{peerConnection, c})
listLock.Unlock()
peerConnection.OnICECandidate(func(i *webrtc.ICECandidate) {
if i == nil {
return
}
candidateString, err := json.Marshal(i.ToJSON())
if err != nil {
log.Println(err)
return
}
if writeErr := c.WriteJSON(&websocketMessage{
Event: "candidate",
Data: string(candidateString),
}); writeErr != nil {
log.Println(writeErr)
}
})
peerConnection.OnConnectionStateChange(func(p webrtc.PeerConnectionState) {
switch p {
case webrtc.PeerConnectionStateFailed:
if err := peerConnection.Close(); err != nil {
log.Print(err)
}
case webrtc.PeerConnectionStateClosed:
signalPeerConnections()
default:
}
})
peerConnection.OnTrack(func(t *webrtc.TrackRemote, _ *webrtc.RTPReceiver) {
trackLocal := addTrack(t)
defer removeTrack(trackLocal)
buf := make([]byte, 1500)
for {
i, _, err := t.Read(buf)
if err != nil {
return
}
if _, err = trackLocal.Write(buf[:i]); err != nil {
return
}
}
})
signalPeerConnections()
message := &websocketMessage{}
for {
_, raw, err := c.ReadMessage()
if err != nil {
log.Println(err)
return
} else if err := json.Unmarshal(raw, &message); err != nil {
log.Println(err)
return
}
switch message.Event {
case "candidate":
candidate := webrtc.ICECandidateInit{}
if err := json.Unmarshal([]byte(message.Data), &candidate); err != nil {
log.Println(err)
return
}
if err := peerConnection.AddICECandidate(candidate); err != nil {
log.Println(err)
return
}
case "answer":
answer := webrtc.SessionDescription{}
if err := json.Unmarshal([]byte(message.Data), &answer); err != nil {
log.Println(err)
return
}
if err := peerConnection.SetRemoteDescription(answer); err != nil {
log.Println(err)
return
}
}
}
}
type threadSafeWriter struct {
*websocket.Conn
sync.Mutex
}
func (t *threadSafeWriter) WriteJSON(v interface{}) error {
t.Lock()
defer t.Unlock()
return t.Conn.WriteJSON(v)
}
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<h3> Local Video </h3>
<video id="localVideo" width="160" height="120" autoplay muted></video>
<br/>
<h3> Remote Video </h3>
<div id="remoteVideos"></div>
<br/>
<h3> Logs </h3>
<div id="logs"></div>
</body>
<script>
navigator.mediaDevices.getUserMedia({video: true, audio: true})
.then(stream => {
let pc = new RTCPeerConnection()
pc.ontrack = function (event) {
if (event.track.kind === 'audio') {
return
}
let el = document.createElement(event.track.kind)
el.srcObject = event.streams[0]
el.autoplay = true
el.controls = true
document.getElementById('remoteVideos').appendChild(el)
event.track.onmute = function (event) {
el.play()
}
event.streams[0].onremovetrack = ({track}) => {
if (el.parentNode) {
el.parentNode.removeChild(el)
}
}
}
document.getElementById('localVideo').srcObject = stream
stream.getTracks().forEach(track => pc.addTrack(track, stream))
let ws = new WebSocket("{{.}}")
pc.onicecandidate = e => {
if (!e.candidate) {
return
}
ws.send(JSON.stringify({event: 'candidate', data: JSON.stringify(e.candidate)}))
}
ws.onclose = function (evt) {
window.alert("Websocket has closed")
}
ws.onmessage = function (evt) {
let msg = JSON.parse(evt.data)
if (!msg) {
return console.log('failed to parse msg')
}
switch (msg.event) {
case 'offer':
let offer = JSON.parse(msg.data)
if (!offer) {
return console.log('failed to parse answer')
}
pc.setRemoteDescription(offer)
pc.createAnswer().then(answer => {
pc.setLocalDescription(answer)
ws.send(JSON.stringify({event: 'answer', data: JSON.stringify(answer)}))
})
return
case 'candidate':
let candidate = JSON.parse(msg.data)
if (!candidate) {
return console.log('failed to parse candidate')
}
pc.addIceCandidate(candidate)
}
}
ws.onerror = function (evt) {
console.log("ERROR: " + evt.data)
}
}).catch(window.alert)
</script>
</html>
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!