"
154 | }
155 | return c.conn.RemoteAddr().String()
156 | }
157 |
--------------------------------------------------------------------------------
/backend/src/signaling/wshub.go:
--------------------------------------------------------------------------------
1 | package signaling
2 |
3 | import (
4 | "encoding/json"
5 |
6 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/conference"
7 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/logging"
8 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/sdp"
9 | )
10 |
11 | // See: https://github.com/gorilla/websocket/tree/master/examples/chat
12 |
13 | // Hub maintains the set of active clients and broadcasts messages to the
14 | // clients.
15 | type WsHub struct {
16 | maxClientId int
17 |
18 | // Registered clients.
19 | clients map[*WsClient]bool
20 |
21 | // Inbound messages from the clients.
22 | messageReceived chan *ReceivedMessage
23 |
24 | broadcast chan BroadcastMessage
25 |
26 | // Register requests from the clients.
27 | register chan *WsClient
28 |
29 | // Unregister requests from clients.
30 | unregister chan *WsClient
31 |
32 | ConferenceManager *conference.ConferenceManager
33 | }
34 |
35 | type BroadcastMessage struct {
36 | Message []byte
37 | ExcludeClients []int
38 | }
39 |
40 | type MessageContainer struct {
41 | Type string `json:"type"`
42 | Data interface{} `json:"data"`
43 | }
44 |
45 | func newWsHub(conferenceManager *conference.ConferenceManager) *WsHub {
46 | return &WsHub{
47 | messageReceived: make(chan *ReceivedMessage),
48 | broadcast: make(chan BroadcastMessage),
49 | register: make(chan *WsClient),
50 | unregister: make(chan *WsClient),
51 | clients: make(map[*WsClient]bool),
52 | ConferenceManager: conferenceManager,
53 | }
54 | }
55 |
56 | func processBroadcastMessage(h *WsHub, broadcastMessage BroadcastMessage) {
57 | for client := range h.clients {
58 | if broadcastMessage.ExcludeClients != nil {
59 | var found = false
60 | for _, item := range broadcastMessage.ExcludeClients {
61 | if item == client.id {
62 | found = true
63 | break
64 | }
65 | }
66 | if found {
67 | continue
68 | }
69 | }
70 | select {
71 | case client.send <- broadcastMessage.Message:
72 | default:
73 | close(client.send)
74 | delete(h.clients, client)
75 | }
76 | }
77 | }
78 |
79 | func writeContainerJSON(client *WsClient, messageType string, messageData interface{}) {
80 | client.conn.WriteJSON(MessageContainer{
81 | Type: messageType,
82 | Data: messageData,
83 | })
84 | }
85 |
86 | func (h *WsHub) run() {
87 | for {
88 | select {
89 | case client := <-h.register:
90 | h.maxClientId++
91 | client.id = h.maxClientId
92 | h.clients[client] = true
93 | logging.Infof(logging.ProtoWS, "A new client connected: client %d (from %s )", client.id, client.conn.RemoteAddr())
94 | logging.Descf(logging.ProtoWS, "Sending welcome message via WebSocket. The client is informed with client ID given by the signaling server.")
95 | writeContainerJSON(client, "Welcome", ClientWelcomeMessage{
96 | Id: client.id,
97 | Message: "Welcome!",
98 | })
99 | case client := <-h.unregister:
100 | if _, ok := h.clients[client]; ok {
101 | delete(h.clients, client)
102 | close(client.send)
103 | logging.Infof(logging.ProtoWS, "Client disconnected: client %d (from %s )", client.id, client.conn.RemoteAddr())
104 | }
105 | case broadcastMessage := <-h.broadcast:
106 | processBroadcastMessage(h, broadcastMessage)
107 | case receivedMessage := <-h.messageReceived:
108 | var messageObj map[string]interface{}
109 | json.Unmarshal(receivedMessage.Message, &messageObj)
110 |
111 | logging.Infof(logging.ProtoWS, "Message received from client %d type %s ", receivedMessage.Sender.id, messageObj["type"])
112 | switch messageObj["type"] {
113 | case "JoinConference":
114 | h.processJoinConference(messageObj["data"].(map[string]interface{}), receivedMessage.Sender)
115 | case "SdpOfferAnswer":
116 | incomingSdpOfferAnswerMessage := sdp.ParseSdpOfferAnswer(messageObj["data"].(map[string]interface{}))
117 | incomingSdpOfferAnswerMessage.ConferenceName = receivedMessage.Sender.conference.ConferenceName
118 | h.ConferenceManager.ChanSdpOffer <- incomingSdpOfferAnswerMessage
119 | /*
120 | processBroadcastMessage(h, BroadcastMessage{
121 | ExcludeClients: []int{receivedMessage.Sender.id},
122 | Message: receivedMessage.Message,
123 | })
124 | */
125 | default:
126 | h.broadcast <- BroadcastMessage{
127 | Message: receivedMessage.Message,
128 | }
129 | }
130 | }
131 | }
132 | }
133 |
134 | func (h *WsHub) processJoinConference(messageData map[string]interface{}, wsClient *WsClient) {
135 | conferenceName := messageData["conferenceName"].(string)
136 | logging.Descf(logging.ProtoWS, "The client %d wanted to join the conference %s .", wsClient.id, conferenceName)
137 | wsClient.conference = h.ConferenceManager.EnsureConference(conferenceName)
138 | logging.Descf(logging.ProtoWS, "The client was joined the conference. Now we should generate an SDP Offer including our UDP candidates (IP-port pairs) and send to the client via Signaling/WebSocket.")
139 | sdpMessage := sdp.GenerateSdpOffer(wsClient.conference.IceAgent)
140 | logging.Infof(logging.ProtoSDP, "Sending SDP Offer to client %d (%s ) for conference %s : %s", wsClient.id, wsClient.RemoteAddrStr(), conferenceName, sdpMessage)
141 | logging.LineSpacer(2)
142 | writeContainerJSON(wsClient, "SdpOffer", sdpMessage)
143 | }
144 |
--------------------------------------------------------------------------------
/backend/src/srtp/cryptogcm.go:
--------------------------------------------------------------------------------
1 | package srtp
2 |
3 | import (
4 | "crypto/aes"
5 | "crypto/cipher"
6 | "encoding/binary"
7 | "errors"
8 |
9 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/rtp"
10 | )
11 |
12 | // https://github.com/pion/srtp/blob/e338637eb5c459e0e43daf9c88cf28dd441eeb7c/context.go#L9
13 | const (
14 | labelSRTPEncryption = 0x00
15 | labelSRTPAuthenticationTag = 0x01
16 | labelSRTPSalt = 0x02
17 |
18 | labelSRTCPEncryption = 0x03
19 | labelSRTCPAuthenticationTag = 0x04
20 | labelSRTCPSalt = 0x05
21 |
22 | seqNumMedian = 1 << 15
23 | seqNumMax = 1 << 16
24 | )
25 |
26 | type GCM struct {
27 | srtpGCM, srtcpGCM cipher.AEAD
28 | srtpSalt, srtcpSalt []byte
29 | }
30 |
31 | // NewGCM creates an SRTP GCM Cipher
32 | func NewGCM(masterKey, masterSalt []byte) (*GCM, error) {
33 | srtpSessionKey, err := aesCmKeyDerivation(labelSRTPEncryption, masterKey, masterSalt, 0, len(masterKey))
34 | if err != nil {
35 | return nil, err
36 | }
37 | srtpBlock, err := aes.NewCipher(srtpSessionKey)
38 | if err != nil {
39 | return nil, err
40 | }
41 |
42 | srtpGCM, err := cipher.NewGCM(srtpBlock)
43 | if err != nil {
44 | return nil, err
45 | }
46 | srtcpSessionKey, err := aesCmKeyDerivation(labelSRTCPEncryption, masterKey, masterSalt, 0, len(masterKey))
47 | if err != nil {
48 | return nil, err
49 | }
50 |
51 | srtcpBlock, err := aes.NewCipher(srtcpSessionKey)
52 | if err != nil {
53 | return nil, err
54 | }
55 |
56 | srtcpGCM, err := cipher.NewGCM(srtcpBlock)
57 | if err != nil {
58 | return nil, err
59 | }
60 |
61 | srtpSalt, err := aesCmKeyDerivation(labelSRTPSalt, masterKey, masterSalt, 0, len(masterSalt))
62 | if err != nil {
63 | return nil, err
64 | }
65 |
66 | srtcpSalt, err := aesCmKeyDerivation(labelSRTCPSalt, masterKey, masterSalt, 0, len(masterSalt))
67 |
68 | if err != nil {
69 | return nil, err
70 | }
71 |
72 | return &GCM{
73 | srtpGCM: srtpGCM,
74 | srtpSalt: srtpSalt,
75 | srtcpGCM: srtcpGCM,
76 | srtcpSalt: srtcpSalt,
77 | }, nil
78 | }
79 |
80 | func (g *GCM) rtpInitializationVector(header *rtp.Header, roc uint32) []byte {
81 | iv := make([]byte, 12)
82 | binary.BigEndian.PutUint32(iv[2:], header.SSRC)
83 | binary.BigEndian.PutUint32(iv[6:], roc)
84 | binary.BigEndian.PutUint16(iv[10:], header.SequenceNumber)
85 |
86 | for i := range iv {
87 | iv[i] ^= g.srtpSalt[i]
88 | }
89 | return iv
90 | }
91 |
92 | func (g *GCM) Decrypt(packet *rtp.Packet, roc uint32) ([]byte, error) {
93 | ciphertext := packet.RawData
94 |
95 | dst := make([]byte, len(ciphertext))
96 | copy(dst, ciphertext)
97 | aeadAuthTagLen := 16
98 | resultLength := len(ciphertext) - aeadAuthTagLen
99 | if resultLength < len(dst) {
100 | dst = dst[:resultLength]
101 | }
102 |
103 | iv := g.rtpInitializationVector(packet.Header, roc)
104 |
105 | if _, err := g.srtpGCM.Open(
106 | dst[packet.HeaderSize:packet.HeaderSize], iv, ciphertext[packet.HeaderSize:], ciphertext[:packet.HeaderSize],
107 | ); err != nil {
108 | return nil, err
109 | }
110 |
111 | copy(dst[:packet.HeaderSize], ciphertext[:packet.HeaderSize])
112 | return dst, nil
113 | }
114 |
115 | // https://github.com/pion/srtp/blob/3c34651fa0c6de900bdc91062e7ccb5992409643/key_derivation.go#L8
116 | func aesCmKeyDerivation(label byte, masterKey, masterSalt []byte, indexOverKdr int, outLen int) ([]byte, error) {
117 | if indexOverKdr != 0 {
118 | // 24-bit "index DIV kdr" must be xored to prf input.
119 | return nil, errors.New("non-zero kdr not supported")
120 | }
121 |
122 | // https://tools.ietf.org/html/rfc3711#appendix-B.3
123 | // The input block for AES-CM is generated by exclusive-oring the master salt with the
124 | // concatenation of the encryption key label 0x00 with (index DIV kdr),
125 | // - index is 'rollover count' and DIV is 'divided by'
126 |
127 | nMasterKey := len(masterKey)
128 | nMasterSalt := len(masterSalt)
129 |
130 | prfIn := make([]byte, nMasterKey)
131 | copy(prfIn[:nMasterSalt], masterSalt)
132 |
133 | prfIn[7] ^= label
134 |
135 | // The resulting value is then AES encrypted using the master key to get the cipher key.
136 | block, err := aes.NewCipher(masterKey)
137 | if err != nil {
138 | return nil, err
139 | }
140 |
141 | out := make([]byte, ((outLen+nMasterKey)/nMasterKey)*nMasterKey)
142 | var i uint16
143 | for n := 0; n < outLen; n += nMasterKey {
144 | binary.BigEndian.PutUint16(prfIn[nMasterKey-2:], i)
145 | block.Encrypt(out[n:n+nMasterKey], prfIn)
146 | i++
147 | }
148 | return out[:outLen], nil
149 | }
150 |
--------------------------------------------------------------------------------
/backend/src/srtp/protectionprofiles.go:
--------------------------------------------------------------------------------
1 | package srtp
2 |
3 | import "fmt"
4 |
5 | type ProtectionProfile uint16
6 |
7 | const (
8 | ProtectionProfile_AEAD_AES_128_GCM ProtectionProfile = ProtectionProfile(0x0007)
9 | )
10 |
11 | type EncryptionKeys struct {
12 | ServerMasterKey []byte
13 | ServerMasterSalt []byte
14 | ClientMasterKey []byte
15 | ClientMasterSalt []byte
16 | }
17 |
18 | func (p ProtectionProfile) String() string {
19 | var result string
20 | switch p {
21 | case ProtectionProfile_AEAD_AES_128_GCM:
22 | result = "SRTP_AEAD_AES_128_GCM"
23 | default:
24 | result = "Unknown SRTP Protection Profile"
25 | }
26 | return fmt.Sprintf("%s (0x%04x)", result, uint16(p))
27 | }
28 |
29 | func (p ProtectionProfile) KeyLength() (int, error) {
30 | switch p {
31 | case ProtectionProfile_AEAD_AES_128_GCM:
32 | return 16, nil
33 | }
34 | return 0, fmt.Errorf("unknown protection profile: %d", p)
35 | }
36 |
37 | func (p ProtectionProfile) SaltLength() (int, error) {
38 | switch p {
39 | case ProtectionProfile_AEAD_AES_128_GCM:
40 | return 12, nil
41 | }
42 | return 0, fmt.Errorf("unknown protection profile: %d", p)
43 | }
44 |
45 | func (p ProtectionProfile) AeadAuthTagLength() (int, error) {
46 | switch p {
47 | case ProtectionProfile_AEAD_AES_128_GCM:
48 | return 16, nil
49 | }
50 | return 0, fmt.Errorf("unknown protection profile: %d", p)
51 | }
52 |
53 | func InitGCM(masterKey, masterSalt []byte) (*GCM, error) {
54 | gcm, err := NewGCM(masterKey, masterSalt)
55 | if err != nil {
56 | return nil, err
57 | }
58 | return gcm, nil
59 | }
60 |
--------------------------------------------------------------------------------
/backend/src/srtp/srtpcontext.go:
--------------------------------------------------------------------------------
1 | package srtp
2 |
3 | import (
4 | "net"
5 |
6 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/rtp"
7 | )
8 |
9 | type SRTPContext struct {
10 | Addr *net.UDPAddr
11 | Conn *net.UDPConn
12 | ProtectionProfile ProtectionProfile
13 | GCM *GCM
14 | srtpSSRCStates map[uint32]*srtpSSRCState
15 | }
16 |
17 | type srtpSSRCState struct {
18 | ssrc uint32
19 | index uint64
20 | rolloverHasProcessed bool
21 | }
22 |
23 | // https://github.com/pion/srtp/blob/3c34651fa0c6de900bdc91062e7ccb5992409643/context.go#L159
24 | func (c *SRTPContext) getSRTPSSRCState(ssrc uint32) *srtpSSRCState {
25 | s, ok := c.srtpSSRCStates[ssrc]
26 | if ok {
27 | return s
28 | }
29 |
30 | s = &srtpSSRCState{
31 | ssrc: ssrc,
32 | }
33 | c.srtpSSRCStates[ssrc] = s
34 | return s
35 | }
36 |
37 | func (s *srtpSSRCState) nextRolloverCount(sequenceNumber uint16) (uint32, func()) {
38 | seq := int32(sequenceNumber)
39 | localRoc := uint32(s.index >> 16)
40 | localSeq := int32(s.index & (seqNumMax - 1))
41 |
42 | guessRoc := localRoc
43 | var difference int32 = 0
44 |
45 | if s.rolloverHasProcessed {
46 | // When localROC is equal to 0, and entering seq-localSeq > seqNumMedian
47 | // judgment, it will cause guessRoc calculation error
48 | if s.index > seqNumMedian {
49 | if localSeq < seqNumMedian {
50 | if seq-localSeq > seqNumMedian {
51 | guessRoc = localRoc - 1
52 | difference = seq - localSeq - seqNumMax
53 | } else {
54 | guessRoc = localRoc
55 | difference = seq - localSeq
56 | }
57 | } else {
58 | if localSeq-seqNumMedian > seq {
59 | guessRoc = localRoc + 1
60 | difference = seq - localSeq + seqNumMax
61 | } else {
62 | guessRoc = localRoc
63 | difference = seq - localSeq
64 | }
65 | }
66 | } else {
67 | // localRoc is equal to 0
68 | difference = seq - localSeq
69 | }
70 | }
71 |
72 | return guessRoc, func() {
73 | if !s.rolloverHasProcessed {
74 | s.index |= uint64(sequenceNumber)
75 | s.rolloverHasProcessed = true
76 | return
77 | }
78 | if difference > 0 {
79 | s.index += uint64(difference)
80 | }
81 | }
82 | }
83 |
84 | // https://github.com/pion/srtp/blob/3c34651fa0c6de900bdc91062e7ccb5992409643/srtp.go#L8
85 | func (c *SRTPContext) DecryptRTPPacket(packet *rtp.Packet) ([]byte, error) {
86 | s := c.getSRTPSSRCState(packet.Header.SSRC)
87 | roc, updateROC := s.nextRolloverCount(packet.Header.SequenceNumber)
88 | result, err := c.GCM.Decrypt(packet, roc)
89 | if err != nil {
90 | return nil, err
91 | }
92 | updateROC()
93 | return result[packet.HeaderSize:], nil
94 | }
95 |
--------------------------------------------------------------------------------
/backend/src/srtp/srtpmanager.go:
--------------------------------------------------------------------------------
1 | package srtp
2 |
3 | import (
4 | "net"
5 |
6 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/logging"
7 | )
8 |
9 | type SRTPManager struct {
10 | }
11 |
12 | func NewSRTPManager() *SRTPManager {
13 | return &SRTPManager{}
14 | }
15 |
16 | func (m *SRTPManager) NewContext(addr *net.UDPAddr, conn *net.UDPConn, protectionProfile ProtectionProfile) *SRTPContext {
17 | result := &SRTPContext{
18 | Addr: addr,
19 | Conn: conn,
20 | ProtectionProfile: protectionProfile,
21 | srtpSSRCStates: map[uint32]*srtpSSRCState{},
22 | }
23 | return result
24 | }
25 |
26 | func (m *SRTPManager) extractEncryptionKeys(protectionProfile ProtectionProfile, keyingMaterial []byte) (*EncryptionKeys, error) {
27 | // https://github.com/pion/srtp/blob/82008b58b1e7be7a0cb834270caafacc7ba53509/keying.go#L14
28 | keyLength, err := protectionProfile.KeyLength()
29 | if err != nil {
30 | return nil, err
31 | }
32 | saltLength, err := protectionProfile.SaltLength()
33 | if err != nil {
34 | return nil, err
35 | }
36 |
37 | offset := 0
38 | clientMasterKey := keyingMaterial[offset : offset+keyLength]
39 | offset += keyLength
40 | serverMasterKey := keyingMaterial[offset : offset+keyLength]
41 | offset += keyLength
42 | clientMasterSalt := keyingMaterial[offset : offset+saltLength]
43 | offset += saltLength
44 | serverMasterSalt := keyingMaterial[offset : offset+saltLength]
45 |
46 | result := &EncryptionKeys{
47 | ClientMasterKey: clientMasterKey,
48 | ClientMasterSalt: clientMasterSalt,
49 | ServerMasterKey: serverMasterKey,
50 | ServerMasterSalt: serverMasterSalt,
51 | }
52 | return result, nil
53 | }
54 |
55 | func (m *SRTPManager) InitCipherSuite(context *SRTPContext, keyingMaterial []byte) error {
56 | logging.Descf(logging.ProtoSRTP, "Initializing SRTP Cipher Suite...")
57 | keys, err := m.extractEncryptionKeys(context.ProtectionProfile, keyingMaterial)
58 | if err != nil {
59 | return err
60 | }
61 | logging.Descf(logging.ProtoSRTP, "Extracted encryption keys from keying material (%d bytes ) [protection profile %s ]\n\tClientMasterKey: 0x%x (%d bytes )\n\tClientMasterSalt: 0x%x (%d bytes )\n\tServerMasterKey: 0x%x (%d bytes )\n\tServerMasterSalt: 0x%x (%d bytes )",
62 | len(keyingMaterial), context.ProtectionProfile,
63 | keys.ClientMasterKey, len(keys.ClientMasterKey),
64 | keys.ClientMasterSalt, len(keys.ClientMasterSalt),
65 | keys.ServerMasterKey, len(keys.ServerMasterKey),
66 | keys.ServerMasterSalt, len(keys.ServerMasterSalt))
67 | logging.Descf(logging.ProtoSRTP, "Initializing GCM using ClientMasterKey and ClientMasterSalt")
68 | gcm, err := InitGCM(keys.ClientMasterKey, keys.ClientMasterSalt)
69 | if err != nil {
70 | return err
71 | }
72 | context.GCM = gcm
73 | return nil
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/backend/src/stun/attribute.go:
--------------------------------------------------------------------------------
1 | package stun
2 |
3 | import (
4 | "encoding/binary"
5 | "fmt"
6 | )
7 |
8 | const (
9 | attributeHeaderSize = 4
10 | )
11 |
12 | type Attribute struct {
13 | AttributeType AttributeType
14 | Value []byte
15 | OffsetInMessage int
16 | }
17 |
18 | func (a *Attribute) GetRawDataLength() int {
19 | return len(a.Value)
20 | }
21 |
22 | func (a *Attribute) GetRawFullLength() int {
23 | return attributeHeaderSize + len(a.Value)
24 | }
25 |
26 | func (a Attribute) String() string {
27 | return fmt.Sprintf("%s: [%s]", a.AttributeType, a.Value)
28 | }
29 |
30 | func DecodeAttribute(buf []byte, offset int, arrayLen int) (*Attribute, error) {
31 | if arrayLen < attributeHeaderSize {
32 | return nil, errIncompleteTURNFrame
33 | }
34 | offsetBackup := offset
35 | attrType := binary.BigEndian.Uint16(buf[offset : offset+2])
36 |
37 | offset += 2
38 |
39 | attrLength := int(binary.BigEndian.Uint16(buf[offset : offset+2]))
40 |
41 | offset += 2
42 |
43 | result := new(Attribute)
44 |
45 | result.OffsetInMessage = offsetBackup
46 | result.AttributeType = AttributeType(attrType)
47 |
48 | result.Value = buf[offset : offset+attrLength]
49 |
50 | return result, nil
51 | }
52 |
53 | func (a *Attribute) Encode() []byte {
54 | attrLen := 4 + len(a.Value)
55 | attrLen += (4 - (attrLen % 4)) % 4
56 | result := make([]byte, attrLen)
57 | binary.BigEndian.PutUint16(result[0:2], uint16(a.AttributeType))
58 | binary.BigEndian.PutUint16(result[2:4], uint16(len(a.Value)))
59 | copy(result[4:], a.Value)
60 | return result
61 | }
62 |
--------------------------------------------------------------------------------
/backend/src/stun/attributes.go:
--------------------------------------------------------------------------------
1 | package stun
2 |
3 | import (
4 | "encoding/binary"
5 | "net"
6 | )
7 |
8 | type IPFamily byte
9 |
10 | const (
11 | IPFamilyIPv4 IPFamily = 0x01
12 | IPFamilyIPV6 IPFamily = 0x02
13 | )
14 |
15 | type MappedAddress struct {
16 | IPFamily IPFamily
17 | IP net.IP
18 | Port uint16
19 | }
20 |
21 | func CreateAttrXorMappedAddress(transactionID []byte, addr *net.UDPAddr) *Attribute {
22 | // https://github.com/jitsi/ice4j/blob/311a495b21f38cc2dfcc4f7118dab96b8134aed6/src/main/java/org/ice4j/attribute/XorMappedAddressAttribute.java#L131
23 | xorMask := make([]byte, 16)
24 | binary.BigEndian.PutUint32(xorMask[0:4], magicCookie)
25 | copy(xorMask[4:], transactionID)
26 | //addressBytes := ms.Addr.IP
27 | portModifier := ((uint16(xorMask[0]) << 8) & 0x0000FF00) | (uint16(xorMask[1]) & 0x000000FF)
28 | addressBytes := make([]byte, len(addr.IP.To4()))
29 | copy(addressBytes, addr.IP.To4())
30 | port := uint16(addr.Port) ^ portModifier
31 | for i := range addressBytes {
32 | addressBytes[i] ^= xorMask[i]
33 | }
34 |
35 | value := make([]byte, 8)
36 |
37 | value[1] = byte(IPFamilyIPv4)
38 | binary.BigEndian.PutUint16(value[2:4], port)
39 | copy(value[4:8], addressBytes)
40 | return &Attribute{
41 | AttributeType: AttrXorMappedAddress,
42 | Value: value,
43 | }
44 | }
45 |
46 | func DecodeAttrXorMappedAddress(attr Attribute, transactionID [12]byte) *MappedAddress {
47 | xorMask := make([]byte, 16)
48 | binary.BigEndian.PutUint32(xorMask[0:4], magicCookie)
49 | copy(xorMask[4:], transactionID[:])
50 |
51 | xorIP := make([]byte, 16)
52 | for i := 0; i < len(attr.Value)-4; i++ {
53 | xorIP[i] = attr.Value[i+4] ^ xorMask[i]
54 | }
55 | family := IPFamily(attr.Value[1])
56 | port := binary.BigEndian.Uint16(attr.Value[2:4])
57 | // Truncate if IPv4, otherwise net.IP sometimes renders it as an IPv6 address.
58 | if family == IPFamilyIPv4 {
59 | xorIP = xorIP[:4]
60 | }
61 | x := binary.BigEndian.Uint16(xorMask[:2])
62 | return &MappedAddress{
63 | IPFamily: family,
64 | IP: net.IP(xorIP),
65 | Port: port ^ x,
66 | }
67 | }
68 |
69 | func CreateAttrUserName(userName string) *Attribute {
70 | return &Attribute{
71 | AttributeType: AttrUserName,
72 | Value: []byte(userName),
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/backend/src/stun/atttributetype.go:
--------------------------------------------------------------------------------
1 | package stun
2 |
3 | import "fmt"
4 |
5 | type AttributeType uint16
6 |
7 | type attributeTypeDef struct {
8 | Name string
9 | }
10 |
11 | func (at AttributeType) String() string {
12 | attributeTypeDef, ok := attributeTypeMap[at]
13 | if !ok {
14 | // Just return hex representation of unknown attribute type.
15 | return fmt.Sprintf("0x%x", uint16(at))
16 | }
17 | return attributeTypeDef.Name
18 | }
19 |
20 | const (
21 | // STUN attributes:
22 |
23 | AttrMappedAddress AttributeType = 0x0001
24 | AttrResponseAddress AttributeType = 0x0002
25 | AttrChangeRequest AttributeType = 0x0003
26 | AttrSourceAddress AttributeType = 0x0004
27 | AttrChangedAddress AttributeType = 0x0005
28 | AttrUserName AttributeType = 0x0006
29 | AttrPassword AttributeType = 0x0007
30 | AttrMessageIntegrity AttributeType = 0x0008
31 | AttrErrorCode AttributeType = 0x0009
32 | AttrUnknownAttributes AttributeType = 0x000a
33 | AttrReflectedFrom AttributeType = 0x000b
34 | AttrRealm AttributeType = 0x0014
35 | AttrNonce AttributeType = 0x0015
36 | AttrXorMappedAddress AttributeType = 0x0020
37 | AttrSoftware AttributeType = 0x8022
38 | AttrAlternameServer AttributeType = 0x8023
39 | AttrFingerprint AttributeType = 0x8028
40 |
41 | // TURN attributes:
42 | AttrChannelNumber AttributeType = 0x000C
43 | AttrLifetime AttributeType = 0x000D
44 | AttrXorPeerAdddress AttributeType = 0x0012
45 | AttrData AttributeType = 0x0013
46 | AttrXorRelayedAddress AttributeType = 0x0016
47 | AttrEvenPort AttributeType = 0x0018
48 | AttrRequestedPort AttributeType = 0x0019
49 | AttrDontFragment AttributeType = 0x001A
50 | AttrReservationRequest AttributeType = 0x0022
51 |
52 | // ICE attributes:
53 | AttrPriority AttributeType = 0x0024
54 | AttrUseCandidate AttributeType = 0x0025
55 | AttrIceControlled AttributeType = 0x8029
56 | AttrIceControlling AttributeType = 0x802A
57 | )
58 |
59 | var attributeTypeMap = map[AttributeType]attributeTypeDef{
60 | // STUN attributes:
61 | AttrMappedAddress: {"MAPPED-ADDRESS"},
62 | AttrResponseAddress: {"RESPONSE-ADDRESS"},
63 | AttrChangeRequest: {"CHANGE-REQUEST"},
64 | AttrSourceAddress: {"SOURCE-ADDRESS"},
65 | AttrChangedAddress: {"CHANGED-ADDRESS"},
66 | AttrUserName: {"USERNAME"},
67 | AttrPassword: {"PASSWORD"},
68 | AttrMessageIntegrity: {"MESSAGE-INTEGRITY"},
69 | AttrErrorCode: {"ERROR-CODE"},
70 | AttrUnknownAttributes: {"UNKNOWN-ATTRIBUTE"},
71 | AttrReflectedFrom: {"REFLECTED-FROM"},
72 | AttrRealm: {"REALM"},
73 | AttrNonce: {"NONCE"},
74 | AttrXorMappedAddress: {"XOR-MAPPED-ADDRES"},
75 | AttrSoftware: {"SOFTWARE"},
76 | AttrAlternameServer: {"ALTERNATE-SERVER"},
77 | AttrFingerprint: {"FINGERPRINT"},
78 |
79 | // TURN attributes:
80 | AttrChannelNumber: {"CHANNEL-NUMBER"},
81 | AttrLifetime: {"LIFETIME"},
82 | AttrXorPeerAdddress: {"XOR-PEER-ADDRESS"},
83 | AttrData: {"DATA"},
84 | AttrXorRelayedAddress: {"XOR-RELAYED-ADDRESS"},
85 | AttrEvenPort: {"EVEN-PORT"},
86 | AttrRequestedPort: {"REQUESTED-TRANSPORT"},
87 | AttrDontFragment: {"DONT-FRAGMENT"},
88 | AttrReservationRequest: {"RESERVATION-TOKEN"},
89 |
90 | // ICE attributes:
91 | AttrPriority: {"PRIORITY"},
92 | AttrUseCandidate: {"USE-CANDIDATE"},
93 | AttrIceControlled: {"ICE-CONTROLLED"},
94 | AttrIceControlling: {"ICE-CONTROLLING"},
95 | }
96 |
--------------------------------------------------------------------------------
/backend/src/stun/message.go:
--------------------------------------------------------------------------------
1 | package stun
2 |
3 | import (
4 | "bytes"
5 | "crypto/hmac"
6 | "crypto/sha1"
7 | "encoding/base64"
8 | "encoding/binary"
9 | "errors"
10 | "fmt"
11 | "hash/crc32"
12 | "strings"
13 |
14 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/config"
15 | )
16 |
17 | var (
18 | //errInvalidTURNFrame = errors.New("data is not a valid TURN frame, no STUN or ChannelData found")
19 | errIncompleteTURNFrame = errors.New("data contains incomplete STUN or TURN frame")
20 | )
21 |
22 | type Message struct {
23 | MessageType MessageType
24 | TransactionID [TransactionIDSize]byte
25 | Attributes map[AttributeType]Attribute
26 | RawMessage []byte
27 | }
28 |
29 | const (
30 | magicCookie = 0x2112A442
31 | messageHeaderSize = 20
32 |
33 | TransactionIDSize = 12 // 96 bit
34 |
35 | stunHeaderSize = 20
36 |
37 | hmacSignatureSize = 20
38 |
39 | fingerprintSize = 4
40 |
41 | fingerprintXorMask = 0x5354554e
42 | )
43 |
44 | func (m Message) String() string {
45 | transactionIDStr := base64.StdEncoding.EncodeToString(m.TransactionID[:])
46 | attrsStr := ""
47 | for _, a := range m.Attributes {
48 | attrsStr += fmt.Sprintf("%s ", strings.ReplaceAll(a.String(), "\r", " "))
49 | }
50 | return fmt.Sprintf("%s id=%s attrs=%s", m.MessageType, transactionIDStr, attrsStr)
51 | }
52 |
53 | func IsMessage(buf []byte, offset int, arrayLen int) bool {
54 | return arrayLen >= messageHeaderSize && binary.BigEndian.Uint32(buf[offset+4:offset+8]) == magicCookie
55 | }
56 |
57 | func DecodeMessage(buf []byte, offset int, arrayLen int) (*Message, error) {
58 | if arrayLen < stunHeaderSize {
59 | return nil, errIncompleteTURNFrame
60 | }
61 |
62 | offsetBackup := offset
63 |
64 | messageType := binary.BigEndian.Uint16(buf[offset : offset+2])
65 |
66 | offset += 2
67 |
68 | messageLength := int(binary.BigEndian.Uint16(buf[offset : offset+2]))
69 |
70 | offset += 2
71 |
72 | // Adding message cookie length
73 | offset += 4
74 |
75 | result := new(Message)
76 |
77 | result.RawMessage = buf[offsetBackup : offsetBackup+arrayLen]
78 |
79 | result.MessageType = decodeMessageType(messageType)
80 |
81 | copy(result.TransactionID[:], buf[offset:offset+TransactionIDSize])
82 |
83 | offset += TransactionIDSize
84 | result.Attributes = map[AttributeType]Attribute{}
85 | for offset-stunHeaderSize < messageLength {
86 | decodedAttr, err := DecodeAttribute(buf, offset, arrayLen)
87 | if err != nil {
88 | return nil, err
89 | }
90 | result.SetAttribute(*decodedAttr)
91 | offset += decodedAttr.GetRawFullLength()
92 |
93 | if decodedAttr.GetRawDataLength()%4 > 0 {
94 | offset += 4 - decodedAttr.GetRawDataLength()%4
95 | }
96 | }
97 | return result, nil
98 | }
99 |
100 | func calculateHmac(binMsg []byte, pwd string) []byte {
101 | key := []byte(pwd)
102 | messageLength := uint16(len(binMsg) + attributeHeaderSize + hmacSignatureSize - messageHeaderSize)
103 | binary.BigEndian.PutUint16(binMsg[2:4], messageLength)
104 | mac := hmac.New(sha1.New, key)
105 | mac.Write(binMsg)
106 | return mac.Sum(nil)
107 | }
108 |
109 | func calculateFingerprint(binMsg []byte) []byte {
110 | result := make([]byte, 4)
111 | messageLength := uint16(len(binMsg) + attributeHeaderSize + fingerprintSize - messageHeaderSize)
112 | binary.BigEndian.PutUint16(binMsg[2:4], messageLength)
113 |
114 | binary.BigEndian.PutUint32(result, crc32.ChecksumIEEE(binMsg)^fingerprintXorMask)
115 | return result
116 | }
117 |
118 | func (m *Message) preEncode() {
119 | // https://github.com/jitsi/ice4j/blob/32a8aadae8fde9b94081f8d002b6fda3490c20dc/src/main/java/org/ice4j/message/Message.java#L1015
120 | delete(m.Attributes, AttrMessageIntegrity)
121 | delete(m.Attributes, AttrFingerprint)
122 | m.Attributes[AttrSoftware] = *createAttrSoftware(config.Val.Server.SoftwareName)
123 | }
124 | func (m *Message) postEncode(encodedMessage []byte, dataLength int, pwd string) []byte {
125 | // https://github.com/jitsi/ice4j/blob/32a8aadae8fde9b94081f8d002b6fda3490c20dc/src/main/java/org/ice4j/message/Message.java#L1015
126 | messageIntegrityAttr := &Attribute{
127 | AttributeType: AttrMessageIntegrity,
128 | Value: calculateHmac(encodedMessage, pwd),
129 | }
130 | encodedMessageIntegrity := messageIntegrityAttr.Encode()
131 | encodedMessage = append(encodedMessage, encodedMessageIntegrity...)
132 |
133 | messageFingerprint := &Attribute{
134 | AttributeType: AttrFingerprint,
135 | Value: calculateFingerprint(encodedMessage),
136 | }
137 | encodedFingerprint := messageFingerprint.Encode()
138 |
139 | encodedMessage = append(encodedMessage, encodedFingerprint...)
140 |
141 | binary.BigEndian.PutUint16(encodedMessage[2:4], uint16(dataLength+len(encodedMessageIntegrity)+len(encodedFingerprint)))
142 |
143 | return encodedMessage
144 | }
145 |
146 | func (m *Message) Encode(pwd string) []byte {
147 | m.preEncode()
148 | // https://github.com/jitsi/ice4j/blob/311a495b21f38cc2dfcc4f7118dab96b8134aed6/src/main/java/org/ice4j/message/Message.java#L907
149 | var encodedAttrs []byte
150 | for _, attr := range m.Attributes {
151 | encodedAttr := attr.Encode()
152 | encodedAttrs = append(encodedAttrs, encodedAttr...)
153 | }
154 |
155 | result := make([]byte, messageHeaderSize+len(encodedAttrs))
156 |
157 | binary.BigEndian.PutUint16(result[0:2], m.MessageType.Encode())
158 | binary.BigEndian.PutUint16(result[2:4], uint16(len(encodedAttrs)))
159 | binary.BigEndian.PutUint32(result[4:8], magicCookie)
160 | copy(result[8:20], m.TransactionID[:])
161 | copy(result[20:], encodedAttrs)
162 | result = m.postEncode(result, len(encodedAttrs), pwd)
163 |
164 | return result
165 | }
166 |
167 | func (m *Message) Validate(ufrag string, pwd string) {
168 | // https://github.com/jitsi/ice4j/blob/311a495b21f38cc2dfcc4f7118dab96b8134aed6/src/main/java/org/ice4j/stack/StunStack.java#L1254
169 | userNameAttr, okUserName := m.Attributes[AttrUserName]
170 | if okUserName {
171 | userName := strings.Split(string(userNameAttr.Value), ":")[0]
172 | if userName != ufrag {
173 | panic("Message not valid: UserName!")
174 | }
175 | }
176 | if messageIntegrityAttr, ok := m.Attributes[AttrMessageIntegrity]; ok {
177 | if !okUserName {
178 | panic("Message not valid: missing username!")
179 | }
180 | binMsg := make([]byte, messageIntegrityAttr.OffsetInMessage)
181 | copy(binMsg, m.RawMessage[0:messageIntegrityAttr.OffsetInMessage])
182 |
183 | calculatedHmac := calculateHmac(binMsg, pwd)
184 | if !bytes.Equal(calculatedHmac, messageIntegrityAttr.Value) {
185 | panic(fmt.Sprintf("Message not valid: MESSAGE-INTEGRITY not valid expected: %v , received: %v not compatible!", calculatedHmac, messageIntegrityAttr.Value))
186 | }
187 | }
188 |
189 | if fingerprintAttr, ok := m.Attributes[AttrFingerprint]; ok {
190 | binMsg := make([]byte, fingerprintAttr.OffsetInMessage)
191 | copy(binMsg, m.RawMessage[0:fingerprintAttr.OffsetInMessage])
192 |
193 | calculatedFingerprint := calculateFingerprint(binMsg)
194 | if !bytes.Equal(calculatedFingerprint, fingerprintAttr.Value) {
195 | panic(fmt.Sprintf("Message not valid: FINGERPRINT not valid expected: %v , received: %v not compatible!", calculatedFingerprint, fingerprintAttr.Value))
196 | }
197 | }
198 | }
199 |
200 | func (m *Message) SetAttribute(attr Attribute) {
201 | m.Attributes[attr.AttributeType] = attr
202 | }
203 |
204 | func createAttrSoftware(software string) *Attribute {
205 | return &Attribute{
206 | AttributeType: AttrSoftware,
207 | Value: []byte(software),
208 | }
209 | }
210 |
211 | func NewMessage(messageType MessageType, transactionID [12]byte) *Message {
212 | result := &Message{
213 | MessageType: messageType,
214 | TransactionID: transactionID,
215 | Attributes: map[AttributeType]Attribute{},
216 | }
217 | return result
218 | }
219 |
--------------------------------------------------------------------------------
/backend/src/stun/messageclass.go:
--------------------------------------------------------------------------------
1 | package stun
2 |
3 | import "fmt"
4 |
5 | type MessageClass byte
6 |
7 | type messageClassDef struct {
8 | Name string
9 | }
10 |
11 | const (
12 | MessageClassRequest MessageClass = 0x00
13 | MessageClassIndication MessageClass = 0x01
14 | MessageClassSuccessResponse MessageClass = 0x02
15 | MessageClassErrorResponse MessageClass = 0x03
16 | )
17 |
18 | var messageClassMap = map[MessageClass]messageClassDef{
19 | MessageClassRequest: {"request"},
20 | MessageClassIndication: {"indication"},
21 | MessageClassSuccessResponse: {"success response"},
22 | MessageClassErrorResponse: {"error response"},
23 | }
24 |
25 | func (mc MessageClass) String() string {
26 | messageClassDef, ok := messageClassMap[mc]
27 | if !ok {
28 | // Just return hex representation of unknown class.
29 | return fmt.Sprintf("0x%x", uint16(mc))
30 | }
31 | return messageClassDef.Name
32 | }
33 |
--------------------------------------------------------------------------------
/backend/src/stun/messagemethod.go:
--------------------------------------------------------------------------------
1 | package stun
2 |
3 | import "fmt"
4 |
5 | type MessageMethod uint16
6 |
7 | type messageMethodDef struct {
8 | Name string
9 | }
10 |
11 | const (
12 | MessageMethodStunBinding MessageMethod = 0x0001
13 | MessageMethodTurnAllocate MessageMethod = 0x0003
14 | MessageMethodTurnRefresh MessageMethod = 0x0004
15 | MessageMethodTurnSend MessageMethod = 0x0006
16 | MessageMethodTurnData MessageMethod = 0x0007
17 | MessageMethodTurnCreatePermission MessageMethod = 0x0008
18 | MessageMethodTurnChannelBind MessageMethod = 0x0009
19 | MessageMethodTurnConnect MessageMethod = 0x000a
20 | MessageMethodTurnConnectionBind MessageMethod = 0x000b
21 | MessageMethodTurnConnectionAttempt MessageMethod = 0x000c
22 | )
23 |
24 | var messageMethodMap = map[MessageMethod]messageMethodDef{
25 | MessageMethodStunBinding: {"STUN Binding"},
26 | MessageMethodTurnAllocate: {"TURN Allocate"},
27 | MessageMethodTurnRefresh: {"TURN Refresh"},
28 | MessageMethodTurnSend: {"TURN Send"},
29 | MessageMethodTurnData: {"TURN Data"},
30 | MessageMethodTurnCreatePermission: {"TURN CreatePermission"},
31 | MessageMethodTurnChannelBind: {"TURN ChannelBind"},
32 | MessageMethodTurnConnect: {"TURN Connect"},
33 | MessageMethodTurnConnectionBind: {"TURN ConnectionBind"},
34 | MessageMethodTurnConnectionAttempt: {"TURN ConnectionAttempt"},
35 | }
36 |
37 | func (mm MessageMethod) String() string {
38 | messageMethodDef, ok := messageMethodMap[mm]
39 | if !ok {
40 | // Just return hex representation of unknown method.
41 | return fmt.Sprintf("0x%x", uint16(mm))
42 | }
43 | return messageMethodDef.Name
44 | }
45 |
--------------------------------------------------------------------------------
/backend/src/stun/messagetype.go:
--------------------------------------------------------------------------------
1 | package stun
2 |
3 | import "fmt"
4 |
5 | type MessageType struct {
6 | MessageMethod MessageMethod
7 | MessageClass MessageClass
8 | }
9 |
10 | func (mt MessageType) String() string {
11 | return fmt.Sprintf("%s %s", mt.MessageMethod, mt.MessageClass)
12 | }
13 |
14 | const (
15 | methodABits = 0xf // 0b0000000000001111
16 | methodBBits = 0x70 // 0b0000000001110000
17 | methodDBits = 0xf80 // 0b0000111110000000
18 |
19 | methodBShift = 1
20 | methodDShift = 2
21 |
22 | firstBit = 0x1
23 | secondBit = 0x2
24 |
25 | c0Bit = firstBit
26 | c1Bit = secondBit
27 |
28 | classC0Shift = 4
29 | classC1Shift = 7
30 | )
31 |
32 | func decodeMessageType(mt uint16) MessageType {
33 | // Decoding class.
34 | // We are taking first bit from v >> 4 and second from v >> 7.
35 | c0 := (mt >> classC0Shift) & c0Bit
36 | c1 := (mt >> classC1Shift) & c1Bit
37 | class := c0 + c1
38 |
39 | // Decoding method.
40 | a := mt & methodABits // A(M0-M3)
41 | b := (mt >> methodBShift) & methodBBits // B(M4-M6)
42 | d := (mt >> methodDShift) & methodDBits // D(M7-M11)
43 | m := a + b + d
44 |
45 | return MessageType{
46 | MessageClass: MessageClass(class),
47 | MessageMethod: MessageMethod(m),
48 | }
49 | }
50 |
51 | func (mt *MessageType) Encode() uint16 {
52 | m := uint16(mt.MessageMethod)
53 | a := m & methodABits // A = M * 0b0000000000001111 (right 4 bits)
54 | b := m & methodBBits // B = M * 0b0000000001110000 (3 bits after A)
55 | d := m & methodDBits // D = M * 0b0000111110000000 (5 bits after B)
56 |
57 | // Shifting to add "holes" for C0 (at 4 bit) and C1 (8 bit).
58 | m = a + (b << methodBShift) + (d << methodDShift)
59 |
60 | // C0 is zero bit of C, C1 is first bit.
61 | // C0 = C * 0b01, C1 = (C * 0b10) >> 1
62 | // Ct = C0 << 4 + C1 << 8.
63 | // Optimizations: "((C * 0b10) >> 1) << 8" as "(C * 0b10) << 7"
64 | // We need C0 shifted by 4, and C1 by 8 to fit "11" and "7" positions
65 | // (see figure 3).
66 | c := uint16(mt.MessageClass)
67 | c0 := (c & c0Bit) << classC0Shift
68 | c1 := (c & c1Bit) << classC1Shift
69 | class := c0 + c1
70 |
71 | return m + class
72 | }
73 |
74 | var (
75 | MessageTypeBindingRequest = MessageType{
76 | MessageMethod: MessageMethodStunBinding,
77 | MessageClass: MessageClassRequest,
78 | }
79 | MessageTypeBindingSuccessResponse = MessageType{
80 | MessageMethod: MessageMethodStunBinding,
81 | MessageClass: MessageClassSuccessResponse,
82 | }
83 | MessageTypeBindingErrorResponse = MessageType{
84 | MessageMethod: MessageMethodStunBinding,
85 | MessageClass: MessageClassErrorResponse,
86 | }
87 | MessageTypeBindingIndication = MessageType{
88 | MessageMethod: MessageMethodStunBinding,
89 | MessageClass: MessageClassIndication,
90 | }
91 | )
92 |
--------------------------------------------------------------------------------
/backend/src/stun/stunclient.go:
--------------------------------------------------------------------------------
1 | package stun
2 |
3 | import (
4 | "bytes"
5 | "crypto/rand"
6 | "net"
7 | )
8 |
9 | type StunClient struct {
10 | ServerAddr string
11 | Ufrag string
12 | Pwd string
13 | }
14 |
15 | func NewStunClient(serverAddr string, ufrag string, pwd string) *StunClient {
16 | return &StunClient{
17 | ServerAddr: serverAddr,
18 | Ufrag: ufrag,
19 | Pwd: pwd,
20 | }
21 | }
22 |
23 | // https://github.com/ccding/go-stun
24 | func (c *StunClient) Discover() (*MappedAddress, error) {
25 | transactionID, err := generateTransactionID()
26 | if err != nil {
27 | return nil, err
28 | }
29 | serverUDPAddr, err := net.ResolveUDPAddr("udp", c.ServerAddr)
30 | if err != nil {
31 | return nil, err
32 | //return NATError, nil, err
33 | }
34 | bindingRequest := createBindingRequest(transactionID)
35 | encodedBindingRequest := bindingRequest.Encode(c.Pwd)
36 | conn, err := net.ListenUDP("udp", nil)
37 | if err != nil {
38 | return nil, err
39 | }
40 | defer conn.Close()
41 | conn.WriteToUDP(encodedBindingRequest, serverUDPAddr)
42 | buf := make([]byte, 1024)
43 |
44 | for {
45 | bufLen, addr, err := conn.ReadFromUDP(buf)
46 | if err != nil {
47 | return nil, err
48 | }
49 | // If requested target server address and responder address not fit, ignore the packet
50 | if !addr.IP.Equal(serverUDPAddr.IP) || addr.Port != serverUDPAddr.Port {
51 | continue
52 | }
53 | stunMessage, stunErr := DecodeMessage(buf, 0, bufLen)
54 | if stunErr != nil {
55 | panic(stunErr)
56 | }
57 | stunMessage.Validate(c.Ufrag, c.Pwd)
58 | if !bytes.Equal(stunMessage.TransactionID[:], transactionID[:]) {
59 | continue
60 | }
61 | xorMappedAddressAttr, ok := stunMessage.Attributes[AttrXorMappedAddress]
62 | if !ok {
63 | continue
64 | }
65 | mappedAddress := DecodeAttrXorMappedAddress(xorMappedAddressAttr, stunMessage.TransactionID)
66 | return mappedAddress, nil
67 | }
68 | }
69 |
70 | func generateTransactionID() ([12]byte, error) {
71 | result := [12]byte{}
72 | _, err := rand.Read(result[:])
73 | if err != nil {
74 | return result, err
75 | }
76 | return result, nil
77 | }
78 |
79 | func createBindingRequest(transactionID [12]byte) *Message {
80 | responseMessage := NewMessage(MessageTypeBindingRequest, transactionID)
81 | return responseMessage
82 | }
83 |
--------------------------------------------------------------------------------
/backend/src/transcoding/vp8.go:
--------------------------------------------------------------------------------
1 | package transcoding
2 |
3 | import (
4 | "bytes"
5 | "errors"
6 | "fmt"
7 | "image/jpeg"
8 | "os"
9 | "path/filepath"
10 |
11 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/logging"
12 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/rtp"
13 | "github.com/xlab/libvpx-go/vpx"
14 | )
15 |
16 | var (
17 | currentFrame []byte
18 | seenKeyFrame = false
19 | )
20 |
21 | // https://stackoverflow.com/questions/68859120/how-to-convert-vp8-interframe-into-image-with-pion-webrtc
22 | type VP8Decoder struct {
23 | src <-chan *rtp.Packet
24 | context *vpx.CodecCtx
25 | iface *vpx.CodecIface
26 | }
27 |
28 | func NewVP8Decoder(src <-chan *rtp.Packet) (*VP8Decoder, error) {
29 | result := &VP8Decoder{
30 | src: src,
31 | context: vpx.NewCodecCtx(),
32 | iface: vpx.DecoderIfaceVP8(),
33 | }
34 | err := vpx.Error(vpx.CodecDecInitVer(result.context, result.iface, nil, 0, vpx.DecoderABIVersion))
35 | if err != nil {
36 | return nil, err
37 | }
38 | return result, nil
39 | }
40 |
41 | var (
42 | fileCount = 0
43 | saveDir = "../output"
44 | saveFilePrefix = "shoot"
45 | )
46 |
47 | func (d *VP8Decoder) Run() {
48 |
49 | newpath := filepath.Join(".", saveDir)
50 | err := os.MkdirAll(newpath, os.ModePerm)
51 |
52 | if err != nil {
53 | panic(err)
54 | }
55 |
56 | packetCounter := 0
57 | //https://stackoverflow.com/questions/68859120/how-to-convert-vp8-interframe-into-image-with-pion-webrtc
58 | for rtpPacket := range d.src {
59 | packetCounter++
60 |
61 | vp8Packet := &VP8Packet{}
62 | vp8Packet.Unmarshal(rtpPacket.Payload)
63 | isKeyFrame := vp8Packet.Payload[0] & 0x01
64 |
65 | switch {
66 | case !seenKeyFrame && isKeyFrame == 1:
67 | continue
68 | case currentFrame == nil && vp8Packet.S != 1:
69 | continue
70 | }
71 |
72 | seenKeyFrame = true
73 | currentFrame = append(currentFrame, vp8Packet.Payload[0:]...)
74 |
75 | if !rtpPacket.Header.Marker {
76 | continue
77 | } else if len(currentFrame) == 0 {
78 | continue
79 | }
80 |
81 | err := vpx.Error(vpx.CodecDecode(d.context, string(currentFrame), uint32(len(currentFrame)), nil, 0))
82 | if err != nil {
83 | logging.Errorf(logging.ProtoVP8, "Error while decoding packet: %s", err)
84 | currentFrame = nil
85 | seenKeyFrame = false
86 | continue
87 | }
88 |
89 | var iter vpx.CodecIter
90 | img := vpx.CodecGetFrame(d.context, &iter)
91 | if img != nil {
92 | img.Deref()
93 |
94 | outputImageFilePath, err := d.saveImageFile(img)
95 | if err != nil {
96 | logging.Errorf(logging.ProtoVP8, "Error while image saving: %s", err)
97 | } else {
98 | logging.Infof(logging.ProtoVP8, "Image file saved: %s\n", outputImageFilePath)
99 | }
100 |
101 | }
102 | currentFrame = nil
103 | seenKeyFrame = false
104 |
105 | }
106 | }
107 |
108 | func (d *VP8Decoder) safeEncodeJpeg(buffer *bytes.Buffer, img *vpx.Image) (err error) {
109 | defer func() {
110 | if r := recover(); r != nil {
111 | err = fmt.Errorf("error: %v, image width: %d, height: %d", r, img.W, img.H)
112 | }
113 | }()
114 | return jpeg.Encode(buffer, img.ImageYCbCr(), nil)
115 | }
116 |
117 | func (d *VP8Decoder) saveImageFile(img *vpx.Image) (string, error) {
118 | fileCount++
119 | buffer := new(bytes.Buffer)
120 | if err := d.safeEncodeJpeg(buffer, img); err != nil {
121 | return "", fmt.Errorf("jpeg Encode Error: %s", err)
122 | }
123 |
124 | outputImageFilePath := fmt.Sprintf("%s%d%s", filepath.Join(saveDir, saveFilePrefix), fileCount, ".jpg")
125 | fo, err := os.Create(outputImageFilePath)
126 |
127 | if err != nil {
128 | return "", fmt.Errorf("image create Error: %s", err)
129 | }
130 | // close fo on exit and check for its returned error
131 | defer func() {
132 | if err := fo.Close(); err != nil {
133 | panic(err)
134 | }
135 | }()
136 |
137 | if _, err := fo.Write(buffer.Bytes()); err != nil {
138 | return "", fmt.Errorf("image write Error: %s", err)
139 | }
140 | return outputImageFilePath, nil
141 | }
142 |
143 | /*
144 | 0 1 2 3 4 5 6 7
145 | +-+-+-+-+-+-+-+-+
146 | |X|R|N|S|PartID | (REQUIRED)
147 | +-+-+-+-+-+-+-+-+
148 | X: |I|L|T|K| RSV | (OPTIONAL)
149 | +-+-+-+-+-+-+-+-+
150 | I: |M| PictureID | (OPTIONAL)
151 | +-+-+-+-+-+-+-+-+
152 | L: | TL0PICIDX | (OPTIONAL)
153 | +-+-+-+-+-+-+-+-+
154 | T/K: |TID|Y| KEYIDX | (OPTIONAL)
155 | +-+-+-+-+-+-+-+-+
156 | */
157 |
158 | // https://tools.ietf.org/id/draft-ietf-payload-vp8-05.html
159 | type VP8Packet struct {
160 | // Required Header
161 | X uint8 /* extended control bits present */
162 | N uint8 /* when set to 1 this frame can be discarded */
163 | S uint8 /* start of VP8 partition */
164 | PID uint8 /* partition index */
165 |
166 | // Extended control bits
167 | I uint8 /* 1 if PictureID is present */
168 | L uint8 /* 1 if TL0PICIDX is present */
169 | T uint8 /* 1 if TID is present */
170 | K uint8 /* 1 if KEYIDX is present */
171 |
172 | // Optional extension
173 | PictureID uint16 /* 8 or 16 bits, picture ID */
174 | TL0PICIDX uint8 /* 8 bits temporal level zero index */
175 | TID uint8 /* 2 bits temporal layer index */
176 | Y uint8 /* 1 bit layer sync bit */
177 | KEYIDX uint8 /* 5 bits temporal key frame index */
178 |
179 | Payload []byte
180 | }
181 |
182 | func (p *VP8Packet) Unmarshal(payload []byte) ([]byte, error) {
183 | if payload == nil {
184 | return nil, errors.New("errNilPacket")
185 | }
186 |
187 | payloadLen := len(payload)
188 |
189 | if payloadLen < 4 {
190 | return nil, errors.New("errShortPacket")
191 | }
192 |
193 | payloadIndex := 0
194 |
195 | p.X = (payload[payloadIndex] & 0x80) >> 7
196 | p.N = (payload[payloadIndex] & 0x20) >> 5
197 | p.S = (payload[payloadIndex] & 0x10) >> 4
198 | p.PID = payload[payloadIndex] & 0x07
199 |
200 | payloadIndex++
201 |
202 | if p.X == 1 {
203 | p.I = (payload[payloadIndex] & 0x80) >> 7
204 | p.L = (payload[payloadIndex] & 0x40) >> 6
205 | p.T = (payload[payloadIndex] & 0x20) >> 5
206 | p.K = (payload[payloadIndex] & 0x10) >> 4
207 | payloadIndex++
208 | }
209 |
210 | if p.I == 1 { // PID present?
211 | if payload[payloadIndex]&0x80 > 0 { // M == 1, PID is 16bit
212 | p.PictureID = (uint16(payload[payloadIndex]&0x7F) << 8) | uint16(payload[payloadIndex+1])
213 | payloadIndex += 2
214 | } else {
215 | p.PictureID = uint16(payload[payloadIndex])
216 | payloadIndex++
217 | }
218 | }
219 |
220 | if payloadIndex >= payloadLen {
221 | return nil, errors.New("errShortPacket")
222 | }
223 |
224 | if p.L == 1 {
225 | p.TL0PICIDX = payload[payloadIndex]
226 | payloadIndex++
227 | }
228 |
229 | if payloadIndex >= payloadLen {
230 | return nil, errors.New("errShortPacket")
231 | }
232 |
233 | if p.T == 1 || p.K == 1 {
234 | if p.T == 1 {
235 | p.TID = payload[payloadIndex] >> 6
236 | p.Y = (payload[payloadIndex] >> 5) & 0x1
237 | }
238 | if p.K == 1 {
239 | p.KEYIDX = payload[payloadIndex] & 0x1F
240 | }
241 | payloadIndex++
242 | }
243 |
244 | if payloadIndex >= payloadLen {
245 | return nil, errors.New("errShortPacket")
246 | }
247 | p.Payload = payload[payloadIndex:]
248 | return p.Payload, nil
249 | }
250 |
--------------------------------------------------------------------------------
/backend/src/udp/udpListener.go:
--------------------------------------------------------------------------------
1 | package udp
2 |
3 | import (
4 | "net"
5 | "strings"
6 | "sync"
7 |
8 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/agent"
9 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/conference"
10 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/logging"
11 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/stun"
12 | )
13 |
14 | type UdpListener struct {
15 | Ip string
16 | Port int
17 |
18 | conn *net.UDPConn
19 |
20 | ConferenceManager *conference.ConferenceManager
21 |
22 | Sockets map[string]*agent.UDPClientSocket
23 | }
24 |
25 | func NewUdpListener(ip string, port int, conferenceManager *conference.ConferenceManager) *UdpListener {
26 | return &UdpListener{
27 | Ip: ip,
28 | Port: port,
29 | ConferenceManager: conferenceManager,
30 | Sockets: map[string]*agent.UDPClientSocket{},
31 | }
32 | }
33 |
34 | func readUfrag(buf []byte, offset int, arrayLen int) (string, string, bool) {
35 | if !stun.IsMessage(buf, offset, arrayLen) {
36 | return "", "", false
37 | }
38 | stunMessage, stunErr := stun.DecodeMessage(buf, 0, arrayLen)
39 | if stunErr != nil {
40 | panic(stunErr)
41 | }
42 | if stunMessage.MessageType != stun.MessageTypeBindingRequest {
43 | return "", "", false
44 | }
45 | userNameAttr, userNameExists := stunMessage.Attributes[stun.AttrUserName]
46 |
47 | if !userNameExists {
48 | return "", "", false
49 | }
50 |
51 | userNameParts := strings.Split(string(userNameAttr.Value), ":")
52 | serverUserName := userNameParts[0]
53 | clientUserName := userNameParts[1]
54 | return serverUserName, clientUserName, true
55 | }
56 |
57 | func (udpListener *UdpListener) Run(waitGroup *sync.WaitGroup) {
58 | defer waitGroup.Done()
59 | conn, err := net.ListenUDP("udp", &net.UDPAddr{
60 | IP: net.IP{0, 0, 0, 0},
61 | Port: udpListener.Port,
62 | })
63 | if err != nil {
64 | panic(err)
65 | }
66 |
67 | udpListener.conn = conn
68 |
69 | defer conn.Close()
70 |
71 | logging.Infof(logging.ProtoUDP, "Listen on %v %d/UDP ", "single-port", udpListener.Port)
72 | logging.Descf(logging.ProtoUDP, "Clients will do media streaming via this connection. This UDP listener acts as demultiplexer, in other words, it can speak in STUN, DTLS, SRTP, SRTCP, etc... protocols in single port. It differentiates protocols by looking shape of packet headers. Clients don't know this listener's IP and port now, they will learn via signaling the SDP offer/answer data when they join a conference via signaling subsystem/WebSocket.")
73 |
74 | buf := make([]byte, 2048)
75 |
76 | // We run into an infinite loop for any incoming UDP packet from specified port.
77 | for {
78 | bufLen, addr, err := conn.ReadFromUDP(buf)
79 | if err == nil {
80 | // Is the client socket known and authenticated by server before?
81 | destinationSocket, ok := udpListener.Sockets[string(addr.IP)+":"+string(rune(addr.Port))]
82 |
83 | if !ok {
84 | logging.Descf(logging.ProtoUDP, "An UDP packet received from %s:%d first time. This client is not known already by UDP server. Is it a STUN binding request?", addr.IP, addr.Port)
85 | // If client socket is not known by server, it can be a STUN binding request.
86 | // Read the server and client ufrag (user fragment) string concatenated via ":" and split it.
87 | serverUfrag, clientUfrag, ok := readUfrag(buf, 0, bufLen)
88 |
89 | if !ok {
90 | logging.Descf(logging.ProtoUDP, "This packet is not a valid STUN binding request, ignore it.")
91 | // If this is not a valid STUN binding request, ignore it.
92 | continue
93 | }
94 | logging.Descf(logging.ProtoUDP, "It is a valid STUN binding request with Server Ufrag: %s , Client Ufrag: %s ", serverUfrag, clientUfrag)
95 | logging.Descf(logging.ProtoUDP, "Looking for valid server agent related with an existing conference, with Ufrag: %s ", serverUfrag)
96 | // It seems a valid STUN binding request, does serverUfrag point
97 | // a server agent (of a defined conference) which is already listening?
98 | agent, ok := udpListener.ConferenceManager.GetAgent(serverUfrag)
99 | if !ok {
100 | logging.Descf(logging.ProtoUDP, "Any server agent couldn't be found that serverUfrag %s points, ignore it.", serverUfrag)
101 | // Any server agent couldn't be found that serverUfrag points, ignore it.
102 | continue
103 | }
104 | logging.Descf(logging.ProtoUDP, "Found server ICE Agent related with conference %s .", agent.ConferenceName)
105 | signalingMediaComponent, ok := agent.SignalingMediaComponents[clientUfrag]
106 | if !ok {
107 | logging.Descf(logging.ProtoUDP, "Client Ufrag %s is not known by server agent %s . SDP Offer/Answer should be processed before UDP STUN binding request. Ignore it.", clientUfrag, serverUfrag)
108 | // Any server agent couldn't be found that serverUfrag points, ignore it.
109 | continue
110 | }
111 | logging.Descf(logging.ProtoUDP, "Found SignalingMediaComponent for client Ufrag %s . It seems we can define a client socket to the server agent(%s ). Creating a new UDPClientSocket object for this UDP client.", signalingMediaComponent.Ufrag, agent.ConferenceName)
112 | // It seems we can define a client socket to the server agent.
113 | destinationSocket = udpListener.addNewUDPClientSocket(agent, addr, clientUfrag)
114 |
115 | // If this STUN binding request's client ufrag is not known by our agent, ignore the request.
116 | // Agent should know the ufrag by SDP offer coming from WebSocket, before coming STUN binding request from UDP.
117 | if destinationSocket == nil {
118 | continue
119 | }
120 | }
121 |
122 | // Now the client socket is known by server, we forward incoming byte array to our socket object dedicated for the client.
123 | destinationSocket.AddBuffer(buf, 0, bufLen)
124 | } else {
125 | logging.Errorf(logging.ProtoUDP, "Some error: %s", err)
126 | }
127 | }
128 |
129 | }
130 |
131 | func (udpListener *UdpListener) addNewUDPClientSocket(serverAgent *agent.ServerAgent, addr *net.UDPAddr, clientUfrag string) *agent.UDPClientSocket {
132 | clientComponent, ok := serverAgent.SignalingMediaComponents[clientUfrag]
133 | if !ok {
134 | // It seems clientUfrag is not known by the server agent, ignore it.
135 | return nil
136 | }
137 | udpClientSocket, err := agent.NewUDPClientSocket(addr, serverAgent.Ufrag, serverAgent.Pwd, clientUfrag, udpListener.conn, clientComponent.FingerprintHash)
138 | if err != nil {
139 | panic(err)
140 | }
141 | serverAgent.Sockets[clientUfrag] = *udpClientSocket
142 | udpListener.Sockets[string(addr.IP)+":"+string(rune(addr.Port))] = udpClientSocket
143 | return udpClientSocket
144 | }
145 |
--------------------------------------------------------------------------------
/docker-compose.dev.yml:
--------------------------------------------------------------------------------
1 | services:
2 | backend:
3 | entrypoint: /entrypoint-dev.sh
4 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | ui:
3 | image: webrtc-nuts-and-bolts/ui
4 | container_name: webrtcnb-ui
5 | build:
6 | context: ui # Dockerfile location
7 | args:
8 | # See for available variants: https://github.com/devcontainers/images/tree/main/src/typescript-node
9 | - VARIANT:22-bookworm
10 | volumes:
11 | # Mount the root folder that contains .git
12 | - "./ui:/workspace:cached"
13 | ports:
14 | - "8080:8080" # Port expose for UI Webpack Dev Server
15 |
16 | backend:
17 | image: webrtc-nuts-and-bolts/backend
18 | container_name: webrtcnb-backend
19 | build:
20 | context: backend # Dockerfile location
21 | args:
22 | # See for available variants: https://hub.docker.com/_/golang?tab=tags
23 | - VARIANT:1.23.0-bookworm
24 | # See: https://code.visualstudio.com/docs/remote/create-dev-container#_set-up-a-folder-to-run-in-a-container
25 | # [Optional] Required for ptrace-based debuggers like C++, Go, and Rust
26 | cap_add:
27 | - SYS_PTRACE
28 | security_opt:
29 | - seccomp:unconfined
30 | volumes:
31 | # Mount the root folder that contains .git
32 | - "./backend:/workspace:cached"
33 | ports:
34 | - "8081:8081" # Port expose for backend WebSocket
35 | - "15000:15000/udp" # Port expose for backend UDP end
--------------------------------------------------------------------------------
/docs/01-RUNNING-IN-DEV-MODE.md:
--------------------------------------------------------------------------------
1 | # **1. RUNNING IN DEVELOPMENT MODE**
2 |
3 | * Clone this repo.
4 |
5 | * Learn your host machine's LAN IP address:
6 |
7 | ```console
8 | $ ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1'
9 |
10 | 192.168.***.***
11 | ```
12 |
13 | * Open [backend/config.yml](../backend/config.yml) file, write your LAN IP address into server/udp/dockerHostIp section.
14 |
15 | * If you don't have VS Code and [Remote Development extension pack](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack) installed, install them. [This link](https://code.visualstudio.com/docs/remote/containers) can be helpful.
16 |
17 | * Start VS Code, open the cloned folder of "webrtc-nuts-and-bolts".
18 |
19 | * Press F1 and select **"Remote Containers: Open Folder in Container..."** then select "backend" folder in "webrtc-nuts-and-bolts".
20 |
21 |
22 |
23 |
24 |
25 | * This command creates (if don't exist) required containers in Docker, then connects inside of webrtcnb-backend container for development and debugging purposes.
26 |
27 | * You will see this notification while building image and starting container. If you click on this notification, you will see a log similar to image below.
28 |
29 |
30 |
31 | 
32 |
33 | * When webrtcnb-backend container has been built and started, VS Code will ask you for some required installations related to Go language, click "Install All" for these prompts.
34 |
35 |
36 |
37 | * After clicking "Install All", you will see installation logs similar to image below.
38 |
39 |
40 |
41 | * When you see "You are ready to Go. :)" message in the log, you can press F5 to run and debug our server inside the container. VS Code can ask for installing other dependencies (like "dlv"), click on "Install" again. If VS Code asked for some extra installations, after installation you may need to press F5 again.
42 |
43 | * You can switch to **"DEBUG CONSOLE"** tab at bottom, you will be able to see the output of running server application:
44 |
45 | 
46 |
47 | * Now your backend server is ready to accept requests from browser!
48 |
49 |
50 |
51 | ---
52 |
53 |
54 |
55 | [< Previous chapter: INFRASTRUCTURE](./00-INFRASTRUCTURE.md) | [Next chapter: BACKEND INITIALIZATION >](./02-BACKEND-INITIALIZATION.md)
56 |
57 |
58 |
--------------------------------------------------------------------------------
/docs/08-VP8-PACKET-DECODE.md:
--------------------------------------------------------------------------------
1 | # **8. VP8 PACKET DECODE**
2 |
3 | In previous chapter, we decoded the SRTP packet and decrypted it successfully, also determined that this packet contains a VP8 video data part.
4 |
5 | * If the packet's PayloadType is in PayloadTypeVP8 type, it forwards to the UDPClientSocket's "vp8Depacketizer" [channel](https://go.dev/tour/concurrency/2). This channel is listened by Run() function of VP8Decoder object in [backend/src/transcoding/vp8.go](../backend/src/transcoding/vp8.go), shown below
6 |
7 | * Our VP8Decoder aims to process incoming VP8 RTP packets, catch and concatenate keyframe packets then convert the result to an image object and save it as a JPEG image file.
8 |
9 | VP8 Payload Descriptor
10 |
11 | ```console
12 | 0 1 2 3 4 5 6 7
13 | +-+-+-+-+-+-+-+-+
14 | |X|R|N|S|PartID | (REQUIRED)
15 | +-+-+-+-+-+-+-+-+
16 | X: |I|L|T|K| RSV | (OPTIONAL)
17 | +-+-+-+-+-+-+-+-+
18 | I: |M| PictureID | (OPTIONAL)
19 | +-+-+-+-+-+-+-+-+
20 | L: | TL0PICIDX | (OPTIONAL)
21 | +-+-+-+-+-+-+-+-+
22 | T/K: |TID|Y| KEYIDX | (OPTIONAL)
23 | +-+-+-+-+-+-+-+-+
24 | ```
25 |
26 | Our VP8 packet struct in [backend/src/transcoding/vp8.go](../backend/src/transcoding/vp8.go)
27 |
28 | from [backend/src/transcoding/vp8.go](../backend/src/transcoding/vp8.go)
29 |
30 | ```go
31 | type VP8Packet struct {
32 | // Required Header
33 | X uint8 /* extended control bits present */
34 | N uint8 /* when set to 1 this frame can be discarded */
35 | S uint8 /* start of VP8 partition */
36 | PID uint8 /* partition index */
37 |
38 | // Extended control bits
39 | I uint8 /* 1 if PictureID is present */
40 | L uint8 /* 1 if TL0PICIDX is present */
41 | T uint8 /* 1 if TID is present */
42 | K uint8 /* 1 if KEYIDX is present */
43 |
44 | // Optional extension
45 | PictureID uint16 /* 8 or 16 bits, picture ID */
46 | TL0PICIDX uint8 /* 8 bits temporal level zero index */
47 | TID uint8 /* 2 bits temporal layer index */
48 | Y uint8 /* 1 bit layer sync bit */
49 | KEYIDX uint8 /* 5 bits temporal key frame index */
50 |
51 | Payload []byte
52 | }
53 | ```
54 |
55 | * We used [libvpx-go](https://github.com/xlab/libvpx-go) as codec, we didn't implement the VP8 codec.
56 |
57 | from [backend/src/transcoding/vp8.go](../backend/src/transcoding/vp8.go)
58 |
59 | ```go
60 | func (d *VP8Decoder) Run() {
61 |
62 | newpath := filepath.Join(".", saveDir)
63 | err := os.MkdirAll(newpath, os.ModePerm)
64 |
65 | if err != nil {
66 | panic(err)
67 | }
68 |
69 | packetCounter := 0
70 | for rtpPacket := range d.src {
71 | packetCounter++
72 |
73 | vp8Packet := &VP8Packet{}
74 | vp8Packet.Unmarshal(rtpPacket.Payload)
75 | isKeyFrame := vp8Packet.Payload[0] & 0x01
76 |
77 | switch {
78 | case !seenKeyFrame && isKeyFrame == 1:
79 | continue
80 | case currentFrame == nil && vp8Packet.S != 1:
81 | continue
82 | }
83 |
84 | seenKeyFrame = true
85 | currentFrame = append(currentFrame, vp8Packet.Payload[0:]...)
86 |
87 | if !rtpPacket.Header.Marker {
88 | continue
89 | } else if len(currentFrame) == 0 {
90 | continue
91 | }
92 |
93 | err := vpx.Error(vpx.CodecDecode(d.context, string(currentFrame), uint32(len(currentFrame)), nil, 0))
94 | if err != nil {
95 | logging.Errorf(logging.ProtoVP8, "Error while decoding packet: %s", err)
96 | currentFrame = nil
97 | seenKeyFrame = false
98 | continue
99 | }
100 |
101 | var iter vpx.CodecIter
102 | img := vpx.CodecGetFrame(d.context, &iter)
103 | if img != nil {
104 | img.Deref()
105 |
106 | outputImageFilePath, err := d.saveImageFile(img)
107 | if err != nil {
108 | logging.Errorf(logging.ProtoVP8, "Error while image saving: %s", err)
109 | } else {
110 | logging.Infof(logging.ProtoVP8, "Image file saved: %s\n", outputImageFilePath)
111 | }
112 |
113 | }
114 | currentFrame = nil
115 | seenKeyFrame = false
116 |
117 | }
118 | }
119 | ```
120 |
121 | * If the function call succeeds,
122 |
123 | ```go
124 | img := vpx.CodecGetFrame(d.context, &iter)
125 | ```
126 |
127 | * We call img.Deref() to convert decoded C structures as Go wrapper variables
128 |
129 | ```go
130 | img.Deref()
131 | ```
132 |
133 | * If everything has gone OK, save the image as a JPEG file by [jpeg.Encode](https://pkg.go.dev/image/jpeg#Encode)
134 | * You can see your caught keyframes at /backend/output/ folder as shoot1.jpg, shoot2.jpg, etc... if multiple keyframes were caught.
135 | * As you can see below, we received multiple RTP packets containing VP8 video data, in different packet lengths, then we caught a keyframe from these packets, concatenate them, then created and saved image.
136 | * At the end of our journey, we saw the "[INFO] Image file saved: ../output/shoot1.jpg" log line! All of our these efforts are to achieve this....
137 |
138 | 
139 |
140 | Sources:
141 |
142 | * [How to convert VP8 interframe into image with Pion/Webrtc? (Stackoverflow)](https://stackoverflow.com/questions/68859120/how-to-convert-vp8-interframe-into-image-with-pion-webrtc)
143 | * [RFC: RTP Payload Format for VP8 Video](https://tools.ietf.org/id/draft-ietf-payload-vp8-05.html)
144 |
145 |
146 |
147 | ---
148 |
149 |
150 |
151 | [< Previous chapter: SRTP PACKETS COME](./07-SRTP-PACKETS-COME.md) | [Next chapter: CONCLUSION >](./09-CONCLUSION.md)
152 |
153 |
154 |
--------------------------------------------------------------------------------
/docs/09-CONCLUSION.md:
--------------------------------------------------------------------------------
1 | # **9. CONCLUSION**
2 |
3 | If you really read all of my journey in this walkthrough documentation, I want to congratulate you for your perseverance, patience, and durability :blush: Also, I want to thank you for your interest.
4 |
5 | If you liked this repo, you can give a star :star: on GitHub. Your support and feedback not only help the project improve and grow but also contribute to reaching a wider audience within the community. Additionally, it motivates me to create even more innovative projects in the future.
6 |
7 | Also thanks to contributors of the awesome sources which were referred during development of this project and writing this documentation. You can find these sources in [README](../README.md), also in between the lines.
8 |
9 | You can find me on [](https://www.linkedin.com/in/alper-dalkiran/) [](https://twitter.com/aalperdalkiran)
10 |
11 |
12 |
13 | Thanks sincerely,
14 |
15 | Adil Alper DALKIRAN
16 |
17 |
18 |
19 |
20 |
21 | ---
22 |
23 |
24 |
25 | [< Previous chapter: VP8 PACKET DECODE](./08-VP8-PACKET-DECODE.md) | [Home >>](../README.md)
26 |
27 |
28 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # **WebRTC Nuts and Bolts: Documentation**
2 |
3 | Here is the adventure of a WebRTC stream from start to finish documented as step by step:
4 |
5 | [0. INFRASTRUCTURE](./00-INFRASTRUCTURE.md)
6 |
7 | [1. RUNNING IN DEVELOPMENT MODE](./01-RUNNING-IN-DEV-MODE.md)
8 |
9 | [2. BACKEND INITIALIZATION](./02-BACKEND-INITIALIZATION.md)
10 |
11 | [3. FIRST CLIENT COMES IN](./03-FIRST-CLIENT-COMES-IN.md)
12 |
13 | [4. STUN BINDING REQUEST FROM CLIENT](./04-STUN-BINDING-REQUEST-FROM-CLIENT.md)
14 |
15 | [5. DTLS HANDSHAKE](./05-DTLS-HANDSHAKE.md)
16 |
17 | [6. SRTP INITIALIZATION](./06-SRTP-INITIALIZATION.md)
18 |
19 | [7. SRTP PACKETS COME](./07-SRTP-PACKETS-COME.md)
20 |
21 | [8. VP8 PACKET DECODE](./08-VP8-PACKET-DECODE.md)
22 |
23 | [9. CONCLUSION](./09-CONCLUSION.md)
24 |
25 | ---
26 |
27 |
28 |
29 | [<< Home: README](../README.md) | [Next chapter: INFRASTRUCTURE >](./00-INFRASTRUCTURE.md)
30 |
31 |
32 |
--------------------------------------------------------------------------------
/docs/images/01-01-open-folder-in-container.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/01-01-open-folder-in-container.png
--------------------------------------------------------------------------------
/docs/images/01-02-select-folder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/01-02-select-folder.png
--------------------------------------------------------------------------------
/docs/images/01-03-starting-dev-container-small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/01-03-starting-dev-container-small.png
--------------------------------------------------------------------------------
/docs/images/01-04-starting-dev-container-log.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/01-04-starting-dev-container-log.png
--------------------------------------------------------------------------------
/docs/images/01-05-install-go-deps-small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/01-05-install-go-deps-small.png
--------------------------------------------------------------------------------
/docs/images/01-06-install-go-deps-log.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/01-06-install-go-deps-log.png
--------------------------------------------------------------------------------
/docs/images/01-07-backend-initial-output.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/01-07-backend-initial-output.png
--------------------------------------------------------------------------------
/docs/images/03-01-browser-first-visit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/03-01-browser-first-visit.png
--------------------------------------------------------------------------------
/docs/images/03-02-browser-ask-permissions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/03-02-browser-ask-permissions.png
--------------------------------------------------------------------------------
/docs/images/03-03-browser-onnegotiationneeded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/03-03-browser-onnegotiationneeded.png
--------------------------------------------------------------------------------
/docs/images/03-04-browser-received-welcome-message.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/03-04-browser-received-welcome-message.png
--------------------------------------------------------------------------------
/docs/images/03-05-server-a-new-client-connected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/03-05-server-a-new-client-connected.png
--------------------------------------------------------------------------------
/docs/images/03-06-browser-received-sdpoffer-message-json.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/03-06-browser-received-sdpoffer-message-json.png
--------------------------------------------------------------------------------
/docs/images/03-07-browser-received-sdpoffer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/03-07-browser-received-sdpoffer.png
--------------------------------------------------------------------------------
/docs/images/03-08-browser-onsignalingstatechange.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/03-08-browser-onsignalingstatechange.png
--------------------------------------------------------------------------------
/docs/images/03-09-browser-generate-sdpanswer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/03-09-browser-generate-sdpanswer.png
--------------------------------------------------------------------------------
/docs/images/03-10-browser-ice-events.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/03-10-browser-ice-events.png
--------------------------------------------------------------------------------
/docs/images/03-11-browser-send-sdp-answer-signaling.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/03-11-browser-send-sdp-answer-signaling.png
--------------------------------------------------------------------------------
/docs/images/03-12-server-receive-sdpanswer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/03-12-server-receive-sdpanswer.png
--------------------------------------------------------------------------------
/docs/images/04-01-server-received-stun-binding-request.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/04-01-server-received-stun-binding-request.png
--------------------------------------------------------------------------------
/docs/images/05-01-received-first-clienthello.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-01-received-first-clienthello.png
--------------------------------------------------------------------------------
/docs/images/05-02-sent-helloverifyrequest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-02-sent-helloverifyrequest.png
--------------------------------------------------------------------------------
/docs/images/05-03-received-second-clienthello.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-03-received-second-clienthello.png
--------------------------------------------------------------------------------
/docs/images/05-04-processed-second-clienthello.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-04-processed-second-clienthello.png
--------------------------------------------------------------------------------
/docs/images/05-05-sent-serverhello.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-05-sent-serverhello.png
--------------------------------------------------------------------------------
/docs/images/05-06-sent-certificate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-06-sent-certificate.png
--------------------------------------------------------------------------------
/docs/images/05-07-sent-serverkeyexchange.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-07-sent-serverkeyexchange.png
--------------------------------------------------------------------------------
/docs/images/05-08-sent-certificaterequest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-08-sent-certificaterequest.png
--------------------------------------------------------------------------------
/docs/images/05-09-sent-serverhellodone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-09-sent-serverhellodone.png
--------------------------------------------------------------------------------
/docs/images/05-10-received-certificate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-10-received-certificate.png
--------------------------------------------------------------------------------
/docs/images/05-11-received-clientkeyexchange.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-11-received-clientkeyexchange.png
--------------------------------------------------------------------------------
/docs/images/05-12-message-concatenation-result.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-12-message-concatenation-result.png
--------------------------------------------------------------------------------
/docs/images/05-13-init-gcm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-13-init-gcm.png
--------------------------------------------------------------------------------
/docs/images/05-14-received-certificateverify.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-14-received-certificateverify.png
--------------------------------------------------------------------------------
/docs/images/05-15-received-changecipherspec.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-15-received-changecipherspec.png
--------------------------------------------------------------------------------
/docs/images/05-16-received-finished.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-16-received-finished.png
--------------------------------------------------------------------------------
/docs/images/05-17-sent-changecipherspec.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-17-sent-changecipherspec.png
--------------------------------------------------------------------------------
/docs/images/05-18-sent-finished.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-18-sent-finished.png
--------------------------------------------------------------------------------
/docs/images/06-01-srtp-initialization.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/06-01-srtp-initialization.png
--------------------------------------------------------------------------------
/docs/images/07-01-received-rtp-packet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/07-01-received-rtp-packet.png
--------------------------------------------------------------------------------
/docs/images/08-01-image-saved.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/08-01-image-saved.png
--------------------------------------------------------------------------------
/docs/images/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/docs/mkdocs/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 | !mkdocs.yml.template
4 | !*.sh
5 | !stylesheets
6 | !stylesheets/**/*
7 | !javascripts
8 | !javascripts/**/*
9 | !assets
10 | !assets/**/*
11 | !extra-content
12 | !extra-content/**/*
13 |
--------------------------------------------------------------------------------
/docs/mkdocs/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/mkdocs/assets/icon.png
--------------------------------------------------------------------------------
/docs/mkdocs/assets/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/docs/mkdocs/execute-mkdocs.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | docker run --rm -it -v ${PWD}/docs:/docs --entrypoint /docs/mkdocs/prepare-mkdocs.sh squidfunk/mkdocs-material
4 | docker run --rm -it -p 8000:8000 -v ${PWD}/docs/mkdocs:/docs squidfunk/mkdocs-material
--------------------------------------------------------------------------------
/docs/mkdocs/javascripts/mathjax.js:
--------------------------------------------------------------------------------
1 | window.MathJax = {
2 | tex: {
3 | inlineMath: [["\\(", "\\)"]],
4 | displayMath: [["\\[", "\\]"]],
5 | processEscapes: true,
6 | processEnvironments: true
7 | },
8 | options: {
9 | ignoreHtmlClass: ".*|",
10 | processHtmlClass: "arithmatex"
11 | }
12 | };
13 |
14 | document$.subscribe(() => {
15 | MathJax.startup.output.clearCache()
16 | MathJax.typesetClear()
17 | MathJax.texReset()
18 | MathJax.typesetPromise()
19 | })
20 |
--------------------------------------------------------------------------------
/docs/mkdocs/mkdocs.yml.template:
--------------------------------------------------------------------------------
1 | site_name: WebRTC Nuts and Bolts
2 | site_url: https://adalkiran.github.io/webrtc-nuts-and-bolts/
3 | site_author: Adil Alper DALKIRAN
4 | site_description: A holistic way of understanding how WebRTC and its protocols run in practice, with code and detailed documentation.
5 | copyright: Copyright © 2022 - present, Adil Alper DALKIRAN. All rights reserved.
6 | repo_url: https://github.com/adalkiran/webrtc-nuts-and-bolts
7 | repo_name: adalkiran/webrtc-nuts-and-bolts
8 |
9 | theme:
10 | name: 'material'
11 | logo: 'assets/icon.svg'
12 | favicon: 'assets/icon.png'
13 | features:
14 | - toc.follow
15 | - toc.integrate
16 | - navigation.tabs
17 | - navigation.tabs.sticky
18 | - navigation.top
19 | - navigation.tracking
20 | - navigation.footer
21 | - content.code.copy
22 | icon:
23 | repo: fontawesome/brands/github
24 | palette:
25 | # Palette toggle for automatic mode
26 | - media: "(prefers-color-scheme)"
27 | toggle:
28 | icon: material/brightness-auto
29 | name: Switch to light mode
30 |
31 | # Palette toggle for light mode
32 | - media: "(prefers-color-scheme: light)"
33 | scheme: default
34 | toggle:
35 | icon: material/weather-night
36 | name: Switch to dark mode
37 |
38 | # Palette toggle for dark mode
39 | - media: "(prefers-color-scheme: dark)"
40 | scheme: slate
41 | toggle:
42 | icon: material/weather-sunny
43 | name: Switch to system preference
44 |
45 | extra_css:
46 | - stylesheets/custom.css
47 |
48 | nav:
49 | - LLaMA Nuts and Bolts: '../llama-nuts-and-bolts'
50 | - 'WebRTC Nuts and Bolts':
51 | {{navigation_placeholder}}
52 | - 'Contact': 'https://www.linkedin.com/in/alper-dalkiran/'
53 |
54 | extra:
55 | social:
56 | - icon: fontawesome/brands/github
57 | name: 'adalkiran'
58 | link: https://github.com/adalkiran
59 | - icon: fontawesome/brands/x-twitter
60 | name: '@aadalkiran'
61 | link: https://www.linkedin.com/in/alper-dalkiran/
62 | - icon: fontawesome/brands/linkedin
63 | name: 'in/alper-dalkiran'
64 | link: https://www.linkedin.com/in/alper-dalkiran/
65 | analytics:
66 | provider: google
67 | property: G-05VMCF3NF0
68 | # consent:
69 | # title: Cookie consent
70 | # description: >-
71 | # We use cookies to recognize your repeated visits and preferences, as well
72 | # as to measure the effectiveness of our documentation and whether users
73 | # find what they're searching for. With your consent, you're helping us to
74 | # make our documentation better.
75 |
76 | markdown_extensions:
77 | - admonition
78 | - toc:
79 | permalink: true
80 | - md_in_html
81 | - pymdownx.arithmatex:
82 | generic: true
83 | inline_syntax: ['dollar']
84 | - pymdownx.highlight:
85 | anchor_linenums: true
86 | line_spans: __span
87 | pygments_lang_class: true
88 | use_pygments: true
89 | - pymdownx.inlinehilite
90 | - pymdownx.snippets
91 | - pymdownx.superfences
92 | - pymdownx.emoji:
93 | emoji_index: !!python/name:material.extensions.emoji.twemoji
94 | emoji_generator: !!python/name:material.extensions.emoji.to_svg
95 |
96 | plugins:
97 | - social
98 |
99 | extra_javascript:
100 | - javascripts/mathjax.js
101 | - https://polyfill.io/v3/polyfill.min.js?features=es6
102 | - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
103 |
--------------------------------------------------------------------------------
/docs/mkdocs/prepare-mkdocs.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 |
5 | cd mkdocs
6 | rm -rf docs && mkdir -p docs
7 | cp -r ../*.md ../images ./stylesheets ./javascripts ./assets ./docs
8 |
9 | rm ./docs/README.md
10 | cp ./extra-content/*.md ./docs
11 |
12 | GITHUB_URL="https://github.com/adalkiran/webrtc-nuts-and-bolts"
13 |
14 | GITHUB_CONTENT_PREFIX="$GITHUB_URL/blob/main"
15 |
16 | nav_items=""
17 |
18 | for file in ./docs/*.md; do
19 |
20 | # Remove footer navigation
21 | sed -i -e ':a;N;$!ba; s/\s* \s*---*.*//g; ta' "$file"
22 |
23 | # Edit same level paths
24 | sed -i -e 's/\](\.\//\](/g' "$file"
25 |
26 | # Edit upper level paths
27 | sed -i -e "s,\(\[[^\[]*\]\)(\.\.\/\([^)]*\)),\1($GITHUB_CONTENT_PREFIX\/\2),g" "$file"
28 | sed -i -e "s,\(\[.*\]\)(\.\.\/\([^)]*\)),\1($GITHUB_CONTENT_PREFIX\/\2),g" "$file"
29 | sed -i -e "s,\(\[.*\]\)(\([^)]*\.ipynb\)),\1($GITHUB_CONTENT_PREFIX\/docs\/\2),g" "$file"
30 |
31 | # Edit img tag paths
32 | sed -i -e 's/src="images\//src="..\/images\//g' "$file"
33 |
34 | # Edit external links
35 | sed -i -e "/\!\[/!s,\[\([^\[]*\)\](http\([^)]*\)),\\1\<\/a\>,g" "$file"
36 | sed -i -e "/\!\[/!s,\[\(.*\)\](http\([^)]*\)),\ \1\<\/a\>,g" "$file"
37 |
38 | file_name=$(basename "$file")
39 | first_line=$(head -1 "$file")
40 | title=$(echo "$first_line" | sed -e 's/^[^\.]*\. \(.*\)\*\*/\1/g')
41 | chapter_num=$(echo "$first_line" | sed -e 's/^[^0-9]*\([0-9]*\)\..*/\1/g')
42 | case $first_line in
43 | "---"*)
44 | nav_items="\n - '$file_name'${nav_items}"
45 | ;;
46 | *)
47 | if [ ${#chapter_num} -lt 3 ]; then
48 | chapter_num=$(expr $chapter_num + 1)
49 | nav_items="${nav_items}\n - '$file_name'"
50 | else
51 | chapter_num=0
52 | nav_items="\n - '$file_name'${nav_items}"
53 | fi
54 | echo "---
55 | title: $title
56 | type: docs
57 | menus:
58 | - main
59 | weight: $chapter_num
60 | ---
61 | " | cat - "$file" > temp && mv temp "$file"
62 | ;;
63 | esac
64 | done
65 |
66 | cat mkdocs.yml.template | sed -e "s/{{navigation_placeholder}}/${nav_items}/g" > mkdocs.yml
67 |
--------------------------------------------------------------------------------
/docs/mkdocs/stylesheets/custom.css:
--------------------------------------------------------------------------------
1 | mjx-container {
2 | font-size: 16px!important;
3 | }
--------------------------------------------------------------------------------
/ui/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | // See: https://code.visualstudio.com/docs/remote/containers-advanced#_connecting-to-multiple-containers-at-once
2 |
3 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
4 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.195.0/containers/javascript-node
5 | {
6 | "name": "WebRTC Nuts and Bolts UI Container",
7 |
8 | "dockerComposeFile": ["../../docker-compose.yml", "../../docker-compose.dev.yml"],
9 | "service": "ui",
10 | "shutdownAction": "none",
11 |
12 |
13 | "workspaceFolder": "/workspace",
14 |
15 |
16 | // Set *default* container specific settings.json values on container create.
17 | "settings": {
18 | },
19 |
20 | // Add the IDs of extensions you want installed when the container is created.
21 | "extensions": [
22 | "dbaeumer.vscode-eslint"
23 | ],
24 |
25 |
26 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
27 | "remoteUser": "node"
28 | }
--------------------------------------------------------------------------------
/ui/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 |
110 | # Serverless directories
111 | .serverless/
112 |
113 | # FuseBox cache
114 | .fusebox/
115 |
116 | # DynamoDB Local files
117 | .dynamodb/
118 |
119 | # TernJS port file
120 | .tern-port
121 |
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 |
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
131 |
132 | yarn.lock
--------------------------------------------------------------------------------
/ui/Dockerfile:
--------------------------------------------------------------------------------
1 | # See here for image contents: https://github.com/devcontainers/images/blob/main/src/typescript-node/.devcontainer/Dockerfile
2 | # See for available variants: https://github.com/devcontainers/images/tree/main/src/typescript-node
3 | ARG VARIANT=22-bookworm
4 | FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:${VARIANT}
5 |
6 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive
7 | # && apt-get -y install --no-install-recommends
8 |
9 | # [Optional] Uncomment if you want to install an additional version of node using nvm
10 | # ARG EXTRA_NODE_VERSION=10
11 | # RUN su node -c "umask 0002 && ./usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
12 |
13 | # [Optional] Uncomment if you want to install more global node modules
14 | # RUN su node -c "npm install -g "
15 |
16 | WORKDIR /workspace
17 |
18 | ENTRYPOINT yarn install && npm run start
--------------------------------------------------------------------------------
/ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webrtc-nuts-and-bolts-ui",
3 | "version": "1.0.0",
4 | "description": "",
5 | "private": true,
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "watch": "webpack --watch",
9 | "start": "webpack serve",
10 | "build": "webpack"
11 | },
12 | "keywords": [],
13 | "author": "",
14 | "license": "ISC",
15 | "devDependencies": {
16 | "@types/sdp-transform": "^2.4.6",
17 | "css-loader": "^6.7.3",
18 | "html-webpack-plugin": "^5.5.0",
19 | "style-loader": "^3.3.2",
20 | "ts-loader": "^9.4.2",
21 | "typescript": "^5.0.2",
22 | "webpack": "^5.76.2",
23 | "webpack-cli": "^5.0.1",
24 | "webpack-dev-server": "^4.13.1"
25 | },
26 | "dependencies": {
27 | "@types/jquery": "^3.5.16",
28 | "jquery": "^3.6.4",
29 | "sdp-transform": "^2.14.1",
30 | "socket.io-client": "^4.6.1"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/ui/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | WebRTC Nuts and Bolts Demo
6 |
7 |
16 |
17 |
18 | Create PeerConnection
19 |
20 | Stop PeerConnection
21 |
22 | Connection status: not connected
23 |
24 |
25 |
--------------------------------------------------------------------------------
/ui/src/sdp.ts:
--------------------------------------------------------------------------------
1 | type MediaType = 'audio' | 'video'
2 | type CandidateType = 'host'
3 | type TransportType = 'udp' | 'tcp'
4 | type FingerprintType = 'sha-256'
5 |
6 |
7 | class SdpMessage {
8 | sessionId: string
9 | mediaItems: SdpMedia[]
10 | }
11 |
12 | class SdpMedia {
13 | mediaId: number
14 | type: MediaType
15 | ufrag: string
16 | pwd: string
17 | fingerprintType: FingerprintType
18 | fingerprintHash: string
19 | candidates: SdpMediaCandidate[]
20 | payloads: string
21 | rtpCodec: string
22 | }
23 |
24 | class SdpMediaCandidate {
25 | ip: string
26 | port: number
27 | type: CandidateType
28 | transport: TransportType
29 | }
30 |
31 | export {SdpMessage}
--------------------------------------------------------------------------------
/ui/src/style/main.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/ui/src/style/main.css
--------------------------------------------------------------------------------
/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "outDir": "./dist/",
5 | "noImplicitAny": true,
6 | "module": "es6",
7 | "target": "es5",
8 | "allowJs": true,
9 | "moduleResolution": "node"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/ui/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const HtmlWebpackPlugin = require("html-webpack-plugin");
3 |
4 | module.exports = {
5 | mode: "development",
6 | entry: {
7 | index: "./src/app.ts",
8 | },
9 | devtool: "inline-source-map",
10 | devServer: {
11 | host: "0.0.0.0",
12 | devMiddleware: {
13 | publicPath: "/"
14 | },
15 | static: {
16 | directory: "./dist"
17 | },
18 | hot: true
19 | },
20 | output: {
21 | filename: "[name].bundle.js",
22 | path: path.resolve(__dirname, "dist"),
23 | clean: true,
24 | },
25 | module: {
26 | rules: [
27 | {
28 | test: /\.tsx?$/,
29 | use: 'ts-loader',
30 | exclude: /node_modules/,
31 | },
32 | {
33 | test: /\.css$/,
34 | use: ["style-loader", "css-loader"]
35 | }
36 | ]
37 | },
38 | plugins: [
39 | new HtmlWebpackPlugin({
40 | hash: true,
41 | template: path.resolve(__dirname, "src", "index.html"),
42 | filename: path.resolve(__dirname, "dist", "index.html")
43 | }),
44 | ],
45 | };
46 |
--------------------------------------------------------------------------------