39 changed files with 1894 additions and 917 deletions
@ -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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
<!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