39 changed files with 1894 additions and 917 deletions
@ -0,0 +1,73 @@
@@ -0,0 +1,73 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/pion/webrtc/v3" |
||||
|
||||
"github.com/aler9/mediamtx/internal/websocket" |
||||
) |
||||
|
||||
type webRTCCandidateReader struct { |
||||
ws *websocket.ServerConn |
||||
|
||||
ctx context.Context |
||||
ctxCancel func() |
||||
|
||||
stopGathering chan struct{} |
||||
readError chan error |
||||
remoteCandidate chan *webrtc.ICECandidateInit |
||||
} |
||||
|
||||
func newWebRTCCandidateReader(ws *websocket.ServerConn) *webRTCCandidateReader { |
||||
ctx, ctxCancel := context.WithCancel(context.Background()) |
||||
|
||||
r := &webRTCCandidateReader{ |
||||
ws: ws, |
||||
ctx: ctx, |
||||
ctxCancel: ctxCancel, |
||||
stopGathering: make(chan struct{}), |
||||
readError: make(chan error), |
||||
remoteCandidate: make(chan *webrtc.ICECandidateInit), |
||||
} |
||||
|
||||
go r.run() |
||||
|
||||
return r |
||||
} |
||||
|
||||
func (r *webRTCCandidateReader) close() { |
||||
r.ctxCancel() |
||||
// do not wait for ReadJSON() to return
|
||||
// it is terminated by ws.Close() later
|
||||
} |
||||
|
||||
func (r *webRTCCandidateReader) run() { |
||||
for { |
||||
candidate, err := r.readCandidate() |
||||
if err != nil { |
||||
select { |
||||
case r.readError <- err: |
||||
case <-r.ctx.Done(): |
||||
} |
||||
return |
||||
} |
||||
|
||||
select { |
||||
case r.remoteCandidate <- candidate: |
||||
case <-r.stopGathering: |
||||
case <-r.ctx.Done(): |
||||
return |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (r *webRTCCandidateReader) readCandidate() (*webrtc.ICECandidateInit, error) { |
||||
var candidate webrtc.ICECandidateInit |
||||
err := r.ws.ReadJSON(&candidate) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &candidate, err |
||||
} |
@ -0,0 +1,133 @@
@@ -0,0 +1,133 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"fmt" |
||||
"time" |
||||
|
||||
"github.com/bluenviron/gortsplib/v3/pkg/formats" |
||||
"github.com/bluenviron/gortsplib/v3/pkg/media" |
||||
"github.com/pion/rtcp" |
||||
"github.com/pion/webrtc/v3" |
||||
) |
||||
|
||||
const ( |
||||
keyFrameInterval = 2 * time.Second |
||||
) |
||||
|
||||
type webRTCIncomingTrack struct { |
||||
track *webrtc.TrackRemote |
||||
receiver *webrtc.RTPReceiver |
||||
writeRTCP func([]rtcp.Packet) error |
||||
|
||||
mediaType media.Type |
||||
format formats.Format |
||||
media *media.Media |
||||
} |
||||
|
||||
func newWebRTCIncomingTrack( |
||||
track *webrtc.TrackRemote, |
||||
receiver *webrtc.RTPReceiver, |
||||
writeRTCP func([]rtcp.Packet) error, |
||||
) (*webRTCIncomingTrack, error) { |
||||
t := &webRTCIncomingTrack{ |
||||
track: track, |
||||
receiver: receiver, |
||||
writeRTCP: writeRTCP, |
||||
} |
||||
|
||||
switch track.Codec().MimeType { |
||||
case webrtc.MimeTypeAV1: |
||||
t.mediaType = media.TypeVideo |
||||
t.format = &formats.AV1{ |
||||
PayloadTyp: uint8(track.PayloadType()), |
||||
} |
||||
|
||||
case webrtc.MimeTypeVP9: |
||||
t.mediaType = media.TypeVideo |
||||
t.format = &formats.VP9{ |
||||
PayloadTyp: uint8(track.PayloadType()), |
||||
} |
||||
|
||||
case webrtc.MimeTypeVP8: |
||||
t.mediaType = media.TypeVideo |
||||
t.format = &formats.VP8{ |
||||
PayloadTyp: uint8(track.PayloadType()), |
||||
} |
||||
|
||||
case webrtc.MimeTypeH264: |
||||
t.mediaType = media.TypeVideo |
||||
t.format = &formats.H264{ |
||||
PayloadTyp: uint8(track.PayloadType()), |
||||
PacketizationMode: 1, |
||||
} |
||||
|
||||
case webrtc.MimeTypeOpus: |
||||
t.mediaType = media.TypeAudio |
||||
t.format = &formats.Opus{ |
||||
PayloadTyp: uint8(track.PayloadType()), |
||||
} |
||||
|
||||
case webrtc.MimeTypeG722: |
||||
t.mediaType = media.TypeAudio |
||||
t.format = &formats.G722{} |
||||
|
||||
case webrtc.MimeTypePCMU: |
||||
t.mediaType = media.TypeAudio |
||||
t.format = &formats.G711{MULaw: true} |
||||
|
||||
case webrtc.MimeTypePCMA: |
||||
t.mediaType = media.TypeAudio |
||||
t.format = &formats.G711{MULaw: false} |
||||
|
||||
default: |
||||
return nil, fmt.Errorf("unsupported codec: %v", track.Codec()) |
||||
} |
||||
|
||||
t.media = &media.Media{ |
||||
Type: t.mediaType, |
||||
Formats: []formats.Format{t.format}, |
||||
} |
||||
|
||||
return t, nil |
||||
} |
||||
|
||||
func (t *webRTCIncomingTrack) start(stream *stream) { |
||||
go func() { |
||||
for { |
||||
pkt, _, err := t.track.ReadRTP() |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
stream.writeRTPPacket(t.media, t.format, pkt, time.Now()) |
||||
} |
||||
}() |
||||
|
||||
// read incoming RTCP packets to make interceptors work
|
||||
go func() { |
||||
buf := make([]byte, 1500) |
||||
for { |
||||
_, _, err := t.receiver.Read(buf) |
||||
if err != nil { |
||||
return |
||||
} |
||||
} |
||||
}() |
||||
|
||||
if t.mediaType == media.TypeVideo { |
||||
go func() { |
||||
keyframeTicker := time.NewTicker(keyFrameInterval) |
||||
|
||||
for range keyframeTicker.C { |
||||
err := t.writeRTCP([]rtcp.Packet{ |
||||
&rtcp.PictureLossIndication{ |
||||
MediaSSRC: uint32(t.track.SSRC()), |
||||
}, |
||||
}) |
||||
if err != nil { |
||||
return |
||||
} |
||||
} |
||||
}() |
||||
} |
||||
} |
@ -0,0 +1,333 @@
@@ -0,0 +1,333 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"time" |
||||
|
||||
"github.com/aler9/mediamtx/internal/formatprocessor" |
||||
"github.com/bluenviron/gortsplib/v3/pkg/formats" |
||||
"github.com/bluenviron/gortsplib/v3/pkg/formats/rtpav1" |
||||
"github.com/bluenviron/gortsplib/v3/pkg/formats/rtph264" |
||||
"github.com/bluenviron/gortsplib/v3/pkg/formats/rtpvp8" |
||||
"github.com/bluenviron/gortsplib/v3/pkg/formats/rtpvp9" |
||||
"github.com/bluenviron/gortsplib/v3/pkg/media" |
||||
"github.com/pion/webrtc/v3" |
||||
) |
||||
|
||||
type webRTCOutgoingTrack struct { |
||||
sender *webrtc.RTPSender |
||||
media *media.Media |
||||
format formats.Format |
||||
track *webrtc.TrackLocalStaticRTP |
||||
cb func(formatprocessor.Unit, context.Context, chan error) |
||||
} |
||||
|
||||
func newWebRTCOutgoingTrackVideo(medias media.Medias) (*webRTCOutgoingTrack, error) { |
||||
var av1Format *formats.AV1 |
||||
av1Media := medias.FindFormat(&av1Format) |
||||
|
||||
if av1Format != nil { |
||||
webRTCTrak, err := webrtc.NewTrackLocalStaticRTP( |
||||
webrtc.RTPCodecCapability{ |
||||
MimeType: webrtc.MimeTypeAV1, |
||||
ClockRate: 90000, |
||||
}, |
||||
"av1", |
||||
"rtspss", |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
encoder := &rtpav1.Encoder{ |
||||
PayloadType: 105, |
||||
PayloadMaxSize: webrtcPayloadMaxSize, |
||||
} |
||||
encoder.Init() |
||||
|
||||
return &webRTCOutgoingTrack{ |
||||
media: av1Media, |
||||
format: av1Format, |
||||
track: webRTCTrak, |
||||
cb: func(unit formatprocessor.Unit, ctx context.Context, writeError chan error) { |
||||
tunit := unit.(*formatprocessor.UnitAV1) |
||||
|
||||
if tunit.OBUs == nil { |
||||
return |
||||
} |
||||
|
||||
packets, err := encoder.Encode(tunit.OBUs, tunit.PTS) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
for _, pkt := range packets { |
||||
webRTCTrak.WriteRTP(pkt) |
||||
} |
||||
}, |
||||
}, nil |
||||
} |
||||
|
||||
var vp9Format *formats.VP9 |
||||
vp9Media := medias.FindFormat(&vp9Format) |
||||
|
||||
if vp9Format != nil { |
||||
webRTCTrak, err := webrtc.NewTrackLocalStaticRTP( |
||||
webrtc.RTPCodecCapability{ |
||||
MimeType: webrtc.MimeTypeVP9, |
||||
ClockRate: uint32(vp9Format.ClockRate()), |
||||
}, |
||||
"vp9", |
||||
"rtspss", |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
encoder := &rtpvp9.Encoder{ |
||||
PayloadType: 96, |
||||
PayloadMaxSize: webrtcPayloadMaxSize, |
||||
} |
||||
encoder.Init() |
||||
|
||||
return &webRTCOutgoingTrack{ |
||||
media: vp9Media, |
||||
format: vp9Format, |
||||
track: webRTCTrak, |
||||
cb: func(unit formatprocessor.Unit, ctx context.Context, writeError chan error) { |
||||
tunit := unit.(*formatprocessor.UnitVP9) |
||||
|
||||
if tunit.Frame == nil { |
||||
return |
||||
} |
||||
|
||||
packets, err := encoder.Encode(tunit.Frame, tunit.PTS) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
for _, pkt := range packets { |
||||
webRTCTrak.WriteRTP(pkt) |
||||
} |
||||
}, |
||||
}, nil |
||||
} |
||||
|
||||
var vp8Format *formats.VP8 |
||||
vp8Media := medias.FindFormat(&vp8Format) |
||||
|
||||
if vp8Format != nil { |
||||
webRTCTrak, err := webrtc.NewTrackLocalStaticRTP( |
||||
webrtc.RTPCodecCapability{ |
||||
MimeType: webrtc.MimeTypeVP8, |
||||
ClockRate: uint32(vp8Format.ClockRate()), |
||||
}, |
||||
"vp8", |
||||
"rtspss", |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
encoder := &rtpvp8.Encoder{ |
||||
PayloadType: 96, |
||||
PayloadMaxSize: webrtcPayloadMaxSize, |
||||
} |
||||
encoder.Init() |
||||
|
||||
return &webRTCOutgoingTrack{ |
||||
media: vp8Media, |
||||
format: vp8Format, |
||||
track: webRTCTrak, |
||||
cb: func(unit formatprocessor.Unit, ctx context.Context, writeError chan error) { |
||||
tunit := unit.(*formatprocessor.UnitVP8) |
||||
|
||||
if tunit.Frame == nil { |
||||
return |
||||
} |
||||
|
||||
packets, err := encoder.Encode(tunit.Frame, tunit.PTS) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
for _, pkt := range packets { |
||||
webRTCTrak.WriteRTP(pkt) |
||||
} |
||||
}, |
||||
}, nil |
||||
} |
||||
|
||||
var h264Format *formats.H264 |
||||
h264Media := medias.FindFormat(&h264Format) |
||||
|
||||
if h264Format != nil { |
||||
webRTCTrak, err := webrtc.NewTrackLocalStaticRTP( |
||||
webrtc.RTPCodecCapability{ |
||||
MimeType: webrtc.MimeTypeH264, |
||||
ClockRate: uint32(h264Format.ClockRate()), |
||||
}, |
||||
"h264", |
||||
"rtspss", |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
encoder := &rtph264.Encoder{ |
||||
PayloadType: 96, |
||||
PayloadMaxSize: webrtcPayloadMaxSize, |
||||
} |
||||
encoder.Init() |
||||
|
||||
var lastPTS time.Duration |
||||
firstNALUReceived := false |
||||
|
||||
return &webRTCOutgoingTrack{ |
||||
media: h264Media, |
||||
format: h264Format, |
||||
track: webRTCTrak, |
||||
cb: func(unit formatprocessor.Unit, ctx context.Context, writeError chan error) { |
||||
tunit := unit.(*formatprocessor.UnitH264) |
||||
|
||||
if tunit.AU == nil { |
||||
return |
||||
} |
||||
|
||||
if !firstNALUReceived { |
||||
firstNALUReceived = true |
||||
lastPTS = tunit.PTS |
||||
} else { |
||||
if tunit.PTS < lastPTS { |
||||
select { |
||||
case writeError <- fmt.Errorf("WebRTC doesn't support H264 streams with B-frames"): |
||||
case <-ctx.Done(): |
||||
} |
||||
return |
||||
} |
||||
lastPTS = tunit.PTS |
||||
} |
||||
|
||||
packets, err := encoder.Encode(tunit.AU, tunit.PTS) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
for _, pkt := range packets { |
||||
webRTCTrak.WriteRTP(pkt) |
||||
} |
||||
}, |
||||
}, nil |
||||
} |
||||
|
||||
return nil, nil |
||||
} |
||||
|
||||
func newWebRTCOutgoingTrackAudio(medias media.Medias) (*webRTCOutgoingTrack, error) { |
||||
var opusFormat *formats.Opus |
||||
opusMedia := medias.FindFormat(&opusFormat) |
||||
|
||||
if opusFormat != nil { |
||||
webRTCTrak, err := webrtc.NewTrackLocalStaticRTP( |
||||
webrtc.RTPCodecCapability{ |
||||
MimeType: webrtc.MimeTypeOpus, |
||||
ClockRate: uint32(opusFormat.ClockRate()), |
||||
}, |
||||
"opus", |
||||
"rtspss", |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &webRTCOutgoingTrack{ |
||||
media: opusMedia, |
||||
format: opusFormat, |
||||
track: webRTCTrak, |
||||
cb: func(unit formatprocessor.Unit, ctx context.Context, writeError chan error) { |
||||
for _, pkt := range unit.GetRTPPackets() { |
||||
webRTCTrak.WriteRTP(pkt) |
||||
} |
||||
}, |
||||
}, nil |
||||
} |
||||
|
||||
var g722Format *formats.G722 |
||||
g722Media := medias.FindFormat(&g722Format) |
||||
|
||||
if g722Format != nil { |
||||
webRTCTrak, err := webrtc.NewTrackLocalStaticRTP( |
||||
webrtc.RTPCodecCapability{ |
||||
MimeType: webrtc.MimeTypeG722, |
||||
ClockRate: uint32(g722Format.ClockRate()), |
||||
}, |
||||
"g722", |
||||
"rtspss", |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &webRTCOutgoingTrack{ |
||||
media: g722Media, |
||||
format: g722Format, |
||||
track: webRTCTrak, |
||||
cb: func(unit formatprocessor.Unit, ctx context.Context, writeError chan error) { |
||||
for _, pkt := range unit.GetRTPPackets() { |
||||
webRTCTrak.WriteRTP(pkt) |
||||
} |
||||
}, |
||||
}, nil |
||||
} |
||||
|
||||
var g711Format *formats.G711 |
||||
g711Media := medias.FindFormat(&g711Format) |
||||
|
||||
if g711Format != nil { |
||||
var mtyp string |
||||
if g711Format.MULaw { |
||||
mtyp = webrtc.MimeTypePCMU |
||||
} else { |
||||
mtyp = webrtc.MimeTypePCMA |
||||
} |
||||
|
||||
webRTCTrak, err := webrtc.NewTrackLocalStaticRTP( |
||||
webrtc.RTPCodecCapability{ |
||||
MimeType: mtyp, |
||||
ClockRate: uint32(g711Format.ClockRate()), |
||||
}, |
||||
"g711", |
||||
"rtspss", |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &webRTCOutgoingTrack{ |
||||
media: g711Media, |
||||
format: g711Format, |
||||
track: webRTCTrak, |
||||
cb: func(unit formatprocessor.Unit, ctx context.Context, writeError chan error) { |
||||
for _, pkt := range unit.GetRTPPackets() { |
||||
webRTCTrak.WriteRTP(pkt) |
||||
} |
||||
}, |
||||
}, nil |
||||
} |
||||
|
||||
return nil, nil |
||||
} |
||||
|
||||
func (t *webRTCOutgoingTrack) start() { |
||||
// read incoming RTCP packets to make interceptors work
|
||||
go func() { |
||||
buf := make([]byte, 1500) |
||||
for { |
||||
_, _, err := t.sender.Read(buf) |
||||
if err != nil { |
||||
return |
||||
} |
||||
} |
||||
}() |
||||
} |
@ -0,0 +1,332 @@
@@ -0,0 +1,332 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"strconv" |
||||
"sync" |
||||
|
||||
"github.com/pion/ice/v2" |
||||
"github.com/pion/interceptor" |
||||
"github.com/pion/webrtc/v3" |
||||
|
||||
"github.com/aler9/mediamtx/internal/logger" |
||||
) |
||||
|
||||
type peerConnection struct { |
||||
*webrtc.PeerConnection |
||||
stateChangeMutex sync.Mutex |
||||
localCandidateRecv chan *webrtc.ICECandidateInit |
||||
connected chan struct{} |
||||
disconnected chan struct{} |
||||
closed chan struct{} |
||||
} |
||||
|
||||
func newPeerConnection( |
||||
videoCodec string, |
||||
audioCodec string, |
||||
iceServers []webrtc.ICEServer, |
||||
iceHostNAT1To1IPs []string, |
||||
iceUDPMux ice.UDPMux, |
||||
iceTCPMux ice.TCPMux, |
||||
log logger.Writer, |
||||
) (*peerConnection, error) { |
||||
configuration := webrtc.Configuration{ICEServers: iceServers} |
||||
settingsEngine := webrtc.SettingEngine{} |
||||
|
||||
if len(iceHostNAT1To1IPs) != 0 { |
||||
settingsEngine.SetNAT1To1IPs(iceHostNAT1To1IPs, webrtc.ICECandidateTypeHost) |
||||
} |
||||
|
||||
if iceUDPMux != nil { |
||||
settingsEngine.SetICEUDPMux(iceUDPMux) |
||||
} |
||||
|
||||
if iceTCPMux != nil { |
||||
settingsEngine.SetICETCPMux(iceTCPMux) |
||||
settingsEngine.SetNetworkTypes([]webrtc.NetworkType{webrtc.NetworkTypeTCP4}) |
||||
} |
||||
|
||||
mediaEngine := &webrtc.MediaEngine{} |
||||
|
||||
if videoCodec != "" || audioCodec != "" { |
||||
switch videoCodec { |
||||
case "av1": |
||||
err := mediaEngine.RegisterCodec( |
||||
webrtc.RTPCodecParameters{ |
||||
RTPCodecCapability: webrtc.RTPCodecCapability{ |
||||
MimeType: webrtc.MimeTypeAV1, |
||||
ClockRate: 90000, |
||||
}, |
||||
PayloadType: 96, |
||||
}, |
||||
webrtc.RTPCodecTypeVideo) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
case "vp9": |
||||
err := mediaEngine.RegisterCodec( |
||||
webrtc.RTPCodecParameters{ |
||||
RTPCodecCapability: webrtc.RTPCodecCapability{ |
||||
MimeType: webrtc.MimeTypeVP9, |
||||
ClockRate: 90000, |
||||
SDPFmtpLine: "profile-id=0", |
||||
}, |
||||
PayloadType: 96, |
||||
}, |
||||
webrtc.RTPCodecTypeVideo) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
err = mediaEngine.RegisterCodec( |
||||
webrtc.RTPCodecParameters{ |
||||
RTPCodecCapability: webrtc.RTPCodecCapability{ |
||||
MimeType: webrtc.MimeTypeVP9, |
||||
ClockRate: 90000, |
||||
SDPFmtpLine: "profile-id=1", |
||||
}, |
||||
PayloadType: 96, |
||||
}, |
||||
webrtc.RTPCodecTypeVideo) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
case "vp8": |
||||
err := mediaEngine.RegisterCodec( |
||||
webrtc.RTPCodecParameters{ |
||||
RTPCodecCapability: webrtc.RTPCodecCapability{ |
||||
MimeType: webrtc.MimeTypeVP8, |
||||
ClockRate: 90000, |
||||
}, |
||||
PayloadType: 96, |
||||
}, |
||||
webrtc.RTPCodecTypeVideo) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
case "h264": |
||||
err := mediaEngine.RegisterCodec( |
||||
webrtc.RTPCodecParameters{ |
||||
RTPCodecCapability: webrtc.RTPCodecCapability{ |
||||
MimeType: webrtc.MimeTypeH264, |
||||
ClockRate: 90000, |
||||
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f", |
||||
}, |
||||
PayloadType: 96, |
||||
}, |
||||
webrtc.RTPCodecTypeVideo) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
switch audioCodec { |
||||
case "opus": |
||||
err := mediaEngine.RegisterCodec( |
||||
webrtc.RTPCodecParameters{ |
||||
RTPCodecCapability: webrtc.RTPCodecCapability{ |
||||
MimeType: webrtc.MimeTypeOpus, |
||||
ClockRate: 48000, |
||||
Channels: 2, |
||||
SDPFmtpLine: "minptime=10;useinbandfec=1", |
||||
}, |
||||
PayloadType: 111, |
||||
}, |
||||
webrtc.RTPCodecTypeAudio) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
case "g722": |
||||
err := mediaEngine.RegisterCodec( |
||||
webrtc.RTPCodecParameters{ |
||||
RTPCodecCapability: webrtc.RTPCodecCapability{ |
||||
MimeType: webrtc.MimeTypeG722, |
||||
ClockRate: 8000, |
||||
}, |
||||
PayloadType: 9, |
||||
}, |
||||
webrtc.RTPCodecTypeAudio) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
case "pcmu": |
||||
err := mediaEngine.RegisterCodec( |
||||
webrtc.RTPCodecParameters{ |
||||
RTPCodecCapability: webrtc.RTPCodecCapability{ |
||||
MimeType: webrtc.MimeTypePCMU, |
||||
ClockRate: 8000, |
||||
}, |
||||
PayloadType: 0, |
||||
}, |
||||
webrtc.RTPCodecTypeAudio) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
case "pcma": |
||||
err := mediaEngine.RegisterCodec( |
||||
webrtc.RTPCodecParameters{ |
||||
RTPCodecCapability: webrtc.RTPCodecCapability{ |
||||
MimeType: webrtc.MimeTypePCMA, |
||||
ClockRate: 8000, |
||||
}, |
||||
PayloadType: 8, |
||||
}, |
||||
webrtc.RTPCodecTypeAudio) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
} else { |
||||
// register all codecs
|
||||
err := mediaEngine.RegisterDefaultCodecs() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
err = mediaEngine.RegisterCodec( |
||||
webrtc.RTPCodecParameters{ |
||||
RTPCodecCapability: webrtc.RTPCodecCapability{ |
||||
MimeType: webrtc.MimeTypeAV1, |
||||
ClockRate: 90000, |
||||
}, |
||||
PayloadType: 105, |
||||
}, |
||||
webrtc.RTPCodecTypeVideo) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
interceptorRegistry := &interceptor.Registry{} |
||||
if err := webrtc.RegisterDefaultInterceptors(mediaEngine, interceptorRegistry); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
api := webrtc.NewAPI( |
||||
webrtc.WithSettingEngine(settingsEngine), |
||||
webrtc.WithMediaEngine(mediaEngine), |
||||
webrtc.WithInterceptorRegistry(interceptorRegistry)) |
||||
|
||||
pc, err := api.NewPeerConnection(configuration) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
co := &peerConnection{ |
||||
PeerConnection: pc, |
||||
localCandidateRecv: make(chan *webrtc.ICECandidateInit), |
||||
connected: make(chan struct{}), |
||||
disconnected: make(chan struct{}), |
||||
closed: make(chan struct{}), |
||||
} |
||||
|
||||
pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { |
||||
co.stateChangeMutex.Lock() |
||||
defer co.stateChangeMutex.Unlock() |
||||
|
||||
select { |
||||
case <-co.closed: |
||||
return |
||||
default: |
||||
} |
||||
|
||||
log.Log(logger.Debug, "peer connection state: "+state.String()) |
||||
|
||||
switch state { |
||||
case webrtc.PeerConnectionStateConnected: |
||||
close(co.connected) |
||||
|
||||
case webrtc.PeerConnectionStateDisconnected: |
||||
close(co.disconnected) |
||||
|
||||
case webrtc.PeerConnectionStateClosed: |
||||
close(co.closed) |
||||
} |
||||
}) |
||||
|
||||
pc.OnICECandidate(func(i *webrtc.ICECandidate) { |
||||
if i != nil { |
||||
v := i.ToJSON() |
||||
select { |
||||
case co.localCandidateRecv <- &v: |
||||
case <-co.connected: |
||||
case <-co.closed: |
||||
} |
||||
} |
||||
}) |
||||
|
||||
return co, nil |
||||
} |
||||
|
||||
func (co *peerConnection) close() { |
||||
co.PeerConnection.Close() |
||||
<-co.closed |
||||
} |
||||
|
||||
func (co *peerConnection) localCandidate() string { |
||||
var cid string |
||||
for _, stats := range co.GetStats() { |
||||
if tstats, ok := stats.(webrtc.ICECandidatePairStats); ok && tstats.Nominated { |
||||
cid = tstats.LocalCandidateID |
||||
break |
||||
} |
||||
} |
||||
|
||||
if cid != "" { |
||||
for _, stats := range co.GetStats() { |
||||
if tstats, ok := stats.(webrtc.ICECandidateStats); ok && tstats.ID == cid { |
||||
return tstats.CandidateType.String() + "/" + tstats.Protocol + "/" + |
||||
tstats.IP + "/" + strconv.FormatInt(int64(tstats.Port), 10) |
||||
} |
||||
} |
||||
} |
||||
|
||||
return "" |
||||
} |
||||
|
||||
func (co *peerConnection) remoteCandidate() string { |
||||
var cid string |
||||
for _, stats := range co.GetStats() { |
||||
if tstats, ok := stats.(webrtc.ICECandidatePairStats); ok && tstats.Nominated { |
||||
cid = tstats.RemoteCandidateID |
||||
break |
||||
} |
||||
} |
||||
|
||||
if cid != "" { |
||||
for _, stats := range co.GetStats() { |
||||
if tstats, ok := stats.(webrtc.ICECandidateStats); ok && tstats.ID == cid { |
||||
return tstats.CandidateType.String() + "/" + tstats.Protocol + "/" + |
||||
tstats.IP + "/" + strconv.FormatInt(int64(tstats.Port), 10) |
||||
} |
||||
} |
||||
} |
||||
|
||||
return "" |
||||
} |
||||
|
||||
func (co *peerConnection) bytesReceived() uint64 { |
||||
for _, stats := range co.GetStats() { |
||||
if tstats, ok := stats.(webrtc.TransportStats); ok { |
||||
if tstats.ID == "iceTransport" { |
||||
return tstats.BytesReceived |
||||
} |
||||
} |
||||
} |
||||
return 0 |
||||
} |
||||
|
||||
func (co *peerConnection) bytesSent() uint64 { |
||||
for _, stats := range co.GetStats() { |
||||
if tstats, ok := stats.(webrtc.TransportStats); ok { |
||||
if tstats.ID == "iceTransport" { |
||||
return tstats.BytesSent |
||||
} |
||||
} |
||||
} |
||||
return 0 |
||||
} |
@ -0,0 +1,374 @@
@@ -0,0 +1,374 @@
|
||||
<!DOCTYPE html> |
||||
<html> |
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<meta name="viewport" content="width=device-width"> |
||||
<style> |
||||
html, body { |
||||
margin: 0; |
||||
padding: 0; |
||||
height: 100%; |
||||
overflow: hidden; |
||||
} |
||||
body { |
||||
display: flex; |
||||
flex-direction: column; |
||||
} |
||||
#video { |
||||
height: 100%; |
||||
background: black; |
||||
flex-grow: 1; |
||||
min-height: 0; |
||||
} |
||||
#controls { |
||||
height: 200px; |
||||
flex-shrink: 0; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
} |
||||
#device { |
||||
flex-direction: column; |
||||
} |
||||
#device > div { |
||||
margin: 10px 0; |
||||
display: flex; |
||||
gap: 20px; |
||||
justify-content: center; |
||||
} |
||||
select { |
||||
width: 200px; |
||||
} |
||||
</style> |
||||
</head> |
||||
<body> |
||||
|
||||
<video id="video" muted controls autoplay playsinline></video> |
||||
<div id="controls"> |
||||
<div id="initializing" style="display: block;"> |
||||
initializing |
||||
</div> |
||||
<div id="device" style="display: none;"> |
||||
<div id="device_line"> |
||||
video device: |
||||
<select id="video_device"> |
||||
<option value="none">none</option> |
||||
</select> |
||||
|
||||
audio device: |
||||
<select id="audio_device"> |
||||
<option value="none">none</option> |
||||
</select> |
||||
</div> |
||||
<div id="codec_line"> |
||||
video codec: |
||||
<select id="video_codec"> |
||||
</select> |
||||
|
||||
audio codec: |
||||
<select id="audio_codec"> |
||||
</select> |
||||
</div> |
||||
<div id="bitrate_line"> |
||||
video bitrate (kbps): |
||||
<input id="video_bitrate" type="text" value="10000" /> |
||||
</div> |
||||
<div id="submit_line"> |
||||
<button id="publish_confirm">publish</button> |
||||
</div> |
||||
</div> |
||||
<div id="transmitting" style="display: none;"> |
||||
publishing |
||||
</div> |
||||
</div> |
||||
|
||||
<script> |
||||
|
||||
const INITIALIZING = 0; |
||||
const DEVICE = 1; |
||||
const TRANSMITTING = 2; |
||||
|
||||
let state = INITIALIZING; |
||||
|
||||
const setState = (newState) => { |
||||
state = newState; |
||||
|
||||
switch (state) { |
||||
case DEVICE: |
||||
document.getElementById("initializing").style.display = 'none'; |
||||
document.getElementById("device").style.display = 'flex'; |
||||
document.getElementById("transmitting").style.display = 'none'; |
||||
break; |
||||
|
||||
case TRANSMITTING: |
||||
document.getElementById("initializing").style.display = 'none'; |
||||
document.getElementById("device").style.display = 'none'; |
||||
document.getElementById("transmitting").style.display = 'flex'; |
||||
break; |
||||
} |
||||
}; |
||||
|
||||
const restartPause = 2000; |
||||
|
||||
class Transmitter { |
||||
constructor(stream) { |
||||
this.stream = stream; |
||||
this.terminated = false; |
||||
this.ws = null; |
||||
this.pc = null; |
||||
this.restartTimeout = null; |
||||
this.start(); |
||||
} |
||||
|
||||
start = () => { |
||||
console.log("connecting"); |
||||
|
||||
const videoCodec = document.getElementById('video_codec').value; |
||||
const audioCodec = document.getElementById('audio_codec').value; |
||||
const videoBitrate = document.getElementById('video_bitrate').value; |
||||
|
||||
const u = window.location.href.replace(/^http/, "ws") + '/ws' + |
||||
'?video_codec=' + videoCodec + |
||||
'&audio_codec=' + audioCodec + |
||||
'&video_bitrate=' + videoBitrate; |
||||
|
||||
this.ws = new WebSocket(u); |
||||
|
||||
this.ws.onerror = () => { |
||||
console.log("ws error"); |
||||
if (this.ws === null) { |
||||
return; |
||||
} |
||||
this.ws.close(); |
||||
this.ws = null; |
||||
}; |
||||
|
||||
this.ws.onclose = () => { |
||||
console.log("ws closed"); |
||||
this.ws = null; |
||||
this.scheduleRestart(); |
||||
}; |
||||
|
||||
this.ws.onmessage = this.onIceServers; |
||||
}; |
||||
|
||||
scheduleRestart = () => { |
||||
if (this.terminated) { |
||||
return; |
||||
} |
||||
|
||||
if (this.ws !== null) { |
||||
this.ws.close(); |
||||
this.ws = null; |
||||
} |
||||
|
||||
if (this.pc !== null) { |
||||
this.pc.close(); |
||||
this.pc = null; |
||||
} |
||||
|
||||
this.restartTimeout = window.setTimeout(() => { |
||||
this.restartTimeout = null; |
||||
this.start(); |
||||
}, restartPause); |
||||
}; |
||||
|
||||
onIceServers = (msg) => { |
||||
if (this.ws === null) { |
||||
return; |
||||
} |
||||
|
||||
this.pc = new RTCPeerConnection({ |
||||
iceServers: JSON.parse(msg.data), |
||||
}); |
||||
|
||||
this.ws.onmessage = this.onOffer; |
||||
}; |
||||
|
||||
onOffer = (msg) => { |
||||
if (this.ws === null || this.pc === null) { |
||||
return; |
||||
} |
||||
|
||||
this.stream.getTracks().forEach((track) => { |
||||
this.pc.addTrack(track, this.stream); |
||||
}); |
||||
|
||||
this.ws.onmessage = (msg) => { |
||||
if (this.pc === null) { |
||||
return; |
||||
} |
||||
this.pc.addIceCandidate(JSON.parse(msg.data)); |
||||
}; |
||||
|
||||
this.pc.onicecandidate = (evt) => { |
||||
if (this.ws === null) { |
||||
return; |
||||
} |
||||
|
||||
if (evt.candidate !== null) { |
||||
if (evt.candidate.candidate !== "") { |
||||
this.ws.send(JSON.stringify(evt.candidate)); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
this.pc.oniceconnectionstatechange = () => { |
||||
if (this.pc === null) { |
||||
return; |
||||
} |
||||
|
||||
console.log("peer connection state:", this.pc.iceConnectionState); |
||||
|
||||
switch (this.pc.iceConnectionState) { |
||||
case "failed": |
||||
case "disconnected": |
||||
this.scheduleRestart(); |
||||
} |
||||
}; |
||||
|
||||
this.pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(msg.data))); |
||||
|
||||
this.pc.createAnswer() |
||||
.then((desc) => { |
||||
if (this.ws === null || this.pc === null) { |
||||
return; |
||||
} |
||||
|
||||
this.pc.setLocalDescription(desc); |
||||
this.ws.send(JSON.stringify(desc)); |
||||
}); |
||||
}; |
||||
} |
||||
|
||||
const onTransmit = (stream) => { |
||||
setState(TRANSMITTING); |
||||
document.getElementById('video').srcObject = stream; |
||||
new Transmitter(stream); |
||||
}; |
||||
|
||||
const onPublish = () => { |
||||
const videoId = document.getElementById('video_device').value; |
||||
const audioId = document.getElementById('audio_device').value; |
||||
|
||||
if (videoId !== 'screen') { |
||||
let video = false; |
||||
if (videoId !== 'none') { |
||||
video = { |
||||
deviceId: videoId, |
||||
}; |
||||
} |
||||
|
||||
let audio = false; |
||||
|
||||
if (audioId !== 'none') { |
||||
audio = { |
||||
deviceId: audioId, |
||||
}; |
||||
} |
||||
|
||||
navigator.mediaDevices.getUserMedia({ video, audio }) |
||||
.then(onTransmit); |
||||
} else { |
||||
navigator.mediaDevices.getDisplayMedia({ |
||||
video: { |
||||
width: { ideal: 1920 }, |
||||
height: { ideal: 1080 }, |
||||
frameRate: { ideal: 30 }, |
||||
cursor: "always", |
||||
}, |
||||
audio: false, |
||||
}) |
||||
.then(onTransmit); |
||||
} |
||||
}; |
||||
|
||||
const populateDevices = () => { |
||||
return navigator.mediaDevices.enumerateDevices() |
||||
.then((devices) => { |
||||
for (const device of devices) { |
||||
switch (device.kind) { |
||||
case 'videoinput': |
||||
{ |
||||
const opt = document.createElement('option'); |
||||
opt.value = device.deviceId; |
||||
opt.text = device.label; |
||||
document.getElementById('video_device').appendChild(opt); |
||||
} |
||||
break; |
||||
|
||||
case 'audioinput': |
||||
{ |
||||
const opt = document.createElement('option'); |
||||
opt.value = device.deviceId; |
||||
opt.text = device.label; |
||||
document.getElementById('audio_device').appendChild(opt); |
||||
} |
||||
break; |
||||
} |
||||
} |
||||
|
||||
// add screen |
||||
const opt = document.createElement('option'); |
||||
opt.value = "screen"; |
||||
opt.text = "screen"; |
||||
document.getElementById('video_device').appendChild(opt); |
||||
|
||||
// set default |
||||
document.getElementById('video_device').value = document.getElementById('video_device').children[1].value; |
||||
if (document.getElementById('audio_device').children.length > 1) { |
||||
document.getElementById('audio_device').value = document.getElementById('audio_device').children[1].value; |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
const populateCodecs = () => { |
||||
const pc = new RTCPeerConnection({}); |
||||
pc.addTransceiver("video", { direction: 'sendonly' }); |
||||
pc.addTransceiver("audio", { direction: 'sendonly' }); |
||||
|
||||
return pc.createOffer() |
||||
.then((desc) => { |
||||
const sdp = desc.sdp.toLowerCase(); |
||||
|
||||
for (const codec of ['av1/90000', 'vp9/90000', 'vp8/90000', 'h264/90000']) { |
||||
if (sdp.includes(codec)) { |
||||
const opt = document.createElement('option'); |
||||
opt.value = codec.split('/')[0]; |
||||
opt.text = codec.split('/')[0].toUpperCase(); |
||||
document.getElementById('video_codec').appendChild(opt); |
||||
} |
||||
} |
||||
|
||||
for (const codec of ['opus/48000', 'g722/8000', 'pcmu/8000', 'pcma/8000']) { |
||||
if (sdp.includes(codec)) { |
||||
const opt = document.createElement('option'); |
||||
opt.value = codec.split('/')[0]; |
||||
opt.text = codec.split('/')[0].toUpperCase(); |
||||
document.getElementById('audio_codec').appendChild(opt); |
||||
} |
||||
} |
||||
|
||||
pc.close(); |
||||
}); |
||||
}; |
||||
|
||||
const initialize = () => { |
||||
navigator.mediaDevices.getUserMedia({ video: true, audio: true }) |
||||
.then(() => Promise.all([ |
||||
populateDevices(), |
||||
populateCodecs(), |
||||
])) |
||||
.then(() => { |
||||
setState(DEVICE); |
||||
}); |
||||
}; |
||||
|
||||
document.getElementById("publish_confirm").addEventListener('click', onPublish); |
||||
|
||||
initialize(); |
||||
|
||||
</script> |
||||
|
||||
</body> |
||||
</html> |
Loading…
Reference in new issue