Browse Source

Signed-off-by: caner <5658514@qq.com>

caner 3 years ago
parent
commit
5d028ad55b

+ 20 - 2
README.md

@@ -1,3 +1,21 @@
-# node-datachannel-examples
+# Examples
 
 
-node-datachannel 数据通道媒体示例
+# client-server
+ * You can use client-server example project to test WebRTC Data Channels with WebSocket signaling.
+ * It uses same logic of [libdatachannel/examples/client](https://github.com/paullouisageneau/libdatachannel/tree/master/examples) project.
+ * Contains an equivalent implementation for a node.js signaling server
+
+## How to Use?
+* Prepare Project
+  * cd examples/client-server
+  * npm i
+* Start ws signaling server;
+  * node signaling-server.js
+* Start answerer (On a new Console);
+  * node client.js
+  * Note local ID
+* Start Offerer (On a new Console);
+  * node client.js
+  * Enter answerer ID
+
+  > You can also use [libdatachannel/examples/client](https://github.com/paullouisageneau/libdatachannel/tree/master/examples) project's client & signaling server

+ 227 - 0
client-server/client-benchmark.js

@@ -0,0 +1,227 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
+const yargs = require('yargs/yargs');
+const { hideBin } = require('yargs/helpers');
+const nodeDataChannel = require('../../lib/index');
+const WebSocket = require('ws');
+const readline = require('readline');
+
+// Init Logger
+nodeDataChannel.initLogger('Debug');
+
+// PeerConnection Map
+const pcMap = {};
+
+const dcArr = [];
+
+// Local ID
+const id = randomId(4);
+
+// Message Size
+const MESSAGE_SIZE = 65535;
+
+// Buffer Size
+const BUFFER_SIZE = MESSAGE_SIZE * 0;
+
+// Args
+const argv = yargs(hideBin(process.argv))
+    .option('disableSend', {
+        type: 'boolean',
+        description: 'Disable Send',
+        default: false,
+    })
+    .option('wsUrl', {
+        type: 'string',
+        description: 'Web Socket URL',
+        default: 'ws://localhost:8000',
+    })
+    .option('dataChannelCount', {
+        type: 'number',
+        description: 'Data Channel Count To Create',
+        default: 1,
+    }).argv;
+
+// Disable Send
+const disableSend = argv.disableSend;
+if (disableSend) console.log('Send Disabled!');
+
+// Signaling Server
+const wsUrl = process.env.WS_URL || argv.wsUrl;
+console.log(wsUrl);
+const dataChannelCount = argv.dataChannelCount;
+
+// Read Line Interface
+const rl = readline.createInterface({
+    input: process.stdin,
+    output: process.stdout,
+});
+
+const ws = new WebSocket(wsUrl + '/' + id, {
+    perMessageDeflate: false,
+});
+
+console.log(`The local ID is: ${id}`);
+console.log(`Waiting for signaling to be connected...`);
+
+ws.on('open', () => {
+    console.log('WebSocket connected, signaling ready');
+    readUserInput();
+});
+
+ws.on('error', (err) => {
+    console.log('WebSocket Error: ', err);
+});
+
+ws.on('message', (msgStr) => {
+    msg = JSON.parse(msgStr);
+    switch (msg.type) {
+        case 'offer':
+            createPeerConnection(msg.id);
+            pcMap[msg.id].setRemoteDescription(msg.description, msg.type);
+            break;
+        case 'answer':
+            pcMap[msg.id].setRemoteDescription(msg.description, msg.type);
+            break;
+        case 'candidate':
+            pcMap[msg.id].addRemoteCandidate(msg.candidate, msg.mid);
+            break;
+
+        default:
+            break;
+    }
+});
+
+function readUserInput() {
+    rl.question('Enter a remote ID to send an offer: ', (peerId) => {
+        if (peerId && peerId.length > 2) {
+            rl.close();
+            console.log('Offering to ', peerId);
+            createPeerConnection(peerId);
+
+            let msgToSend = randomId(MESSAGE_SIZE);
+
+            for (let i = 1; i <= dataChannelCount; i++) {
+                let dcArrItem = {
+                    dc: null,
+                    bytesSent: 0,
+                    bytesReceived: 0,
+                };
+                console.log('Creating DataChannel with label "test-' + i + '"');
+                dcArrItem.dc = pcMap[peerId].createDataChannel('test-' + i);
+
+                dcArrItem.dc.setBufferedAmountLowThreshold(BUFFER_SIZE);
+                dcArrItem.dc.onOpen(() => {
+                    while (!disableSend && dcArrItem.dc.bufferedAmount() <= BUFFER_SIZE) {
+                        dcArrItem.dc.sendMessage(msgToSend);
+                        dcArrItem.bytesSent += msgToSend.length;
+                    }
+                });
+
+                dcArrItem.dc.onBufferedAmountLow(() => {
+                    while (!disableSend && dcArrItem.dc.bufferedAmount() <= BUFFER_SIZE) {
+                        dcArrItem.dc.sendMessage(msgToSend);
+                        dcArrItem.bytesSent += msgToSend.length;
+                    }
+                });
+
+                dcArrItem.dc.onMessage((msg) => {
+                    dcArrItem.bytesReceived += msg.length;
+                });
+
+                dcArr.push(dcArrItem);
+            }
+
+            // Report
+            let i = 0;
+            setInterval(() => {
+                let totalBytesSent = 0;
+                let totalBytesReceived = 0;
+                for (let j = 0; j < dcArr.length; j++) {
+                    console.log(
+                        `${j == 0 ? i + '#' : ''} DC-${j + 1} Sent: ${byte2KB(
+                            dcArr[j].bytesSent,
+                        )} KB/s / Received: ${byte2KB(dcArr[j].bytesReceived)} KB/s / SendBufferAmount: ${dcArr[
+                            j
+                        ].dc.bufferedAmount()} / DataChannelOpen: ${dcArr[j].dc.isOpen()}`,
+                    );
+                    totalBytesSent += dcArr[j].bytesSent;
+                    totalBytesReceived += dcArr[j].bytesReceived;
+                    dcArr[j].bytesSent = 0;
+                    dcArr[j].bytesReceived = 0;
+                }
+                console.log(
+                    `Total Sent: ${byte2KB(totalBytesSent)}  KB/s / Total Received: ${byte2KB(
+                        totalBytesReceived,
+                    )}  KB/s`,
+                );
+
+                if (i % 5 == 0) {
+                    console.log(
+                        `Stats# Sent: ${byte2MB(pcMap[peerId].bytesSent())} MB / Received: ${byte2MB(
+                            pcMap[peerId].bytesReceived(),
+                        )} MB / rtt: ${pcMap[peerId].rtt()} ms`,
+                    );
+                    console.log(`Selected Candidates# ${JSON.stringify(pcMap[peerId].getSelectedCandidatePair())}`);
+                    console.log(``);
+                }
+                i++;
+            }, 1000);
+        }
+    });
+}
+
+function createPeerConnection(peerId) {
+    // Create PeerConnection
+    let peerConnection = new nodeDataChannel.PeerConnection('pc', { iceServers: ['stun:stun.l.google.com:19302'] });
+    peerConnection.onStateChange((state) => {
+        console.log('State: ', state);
+    });
+    peerConnection.onGatheringStateChange((state) => {
+        console.log('GatheringState: ', state);
+    });
+    peerConnection.onLocalDescription((description, type) => {
+        ws.send(JSON.stringify({ id: peerId, type, description }));
+    });
+    peerConnection.onLocalCandidate((candidate, mid) => {
+        ws.send(JSON.stringify({ id: peerId, type: 'candidate', candidate, mid }));
+    });
+    peerConnection.onDataChannel((dc) => {
+        rl.close();
+        console.log('DataChannel from ' + peerId + ' received with label "', dc.getLabel() + '"');
+
+        let msgToSend = randomId(MESSAGE_SIZE);
+
+        while (!disableSend && dc.bufferedAmount() <= BUFFER_SIZE) {
+            dc.sendMessage(msgToSend);
+        }
+
+        dc.onBufferedAmountLow(() => {
+            while (!disableSend && dc.bufferedAmount() <= BUFFER_SIZE) {
+                dc.sendMessage(msgToSend);
+            }
+        });
+
+        dc.onMessage((msg) => {
+            // bytesReceived += msg.length;
+        });
+    });
+
+    pcMap[peerId] = peerConnection;
+}
+
+function randomId(length) {
+    var result = '';
+    var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+    var charactersLength = characters.length;
+    for (var i = 0; i < length; i++) {
+        result += characters.charAt(Math.floor(Math.random() * charactersLength));
+    }
+    return result;
+}
+
+function byte2KB(bytes) {
+    return `${Math.round(bytes / 1000)}`;
+}
+
+function byte2MB(bytes) {
+    return `${Math.round(bytes / (1000 * 1000))}`;
+}

+ 192 - 0
client-server/client-periodic.js

@@ -0,0 +1,192 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
+const nodeDataChannel = require('../../lib/index');
+const WebSocket = require('ws');
+const readline = require('readline');
+
+// Init Logger
+nodeDataChannel.initLogger('Debug');
+
+// PeerConnection Map
+const pcMap = {};
+
+// Local ID
+const id = randomId(4);
+
+// Message Size
+const MESSAGE_SIZE = 500;
+
+// Buffer Size
+const BUFFER_SIZE = MESSAGE_SIZE * 10;
+
+// Read Line Interface
+const rl = readline.createInterface({
+    input: process.stdin,
+    output: process.stdout,
+});
+
+// Signaling Server
+const WS_URL = process.env.WS_URL || 'ws://localhost:8000';
+const ws = new WebSocket(WS_URL + '/' + id, {
+    perMessageDeflate: false,
+});
+
+console.log(`The local ID is: ${id}`);
+console.log(`Waiting for signaling to be connected...`);
+
+ws.on('open', () => {
+    console.log('WebSocket connected, signaling ready');
+    readUserInput();
+});
+
+ws.on('error', (err) => {
+    console.log('WebSocket Error: ', err);
+});
+
+ws.on('message', (msgStr) => {
+    msg = JSON.parse(msgStr);
+    switch (msg.type) {
+        case 'offer':
+            createPeerConnection(msg.id);
+            pcMap[msg.id].setRemoteDescription(msg.description, msg.type);
+            break;
+        case 'answer':
+            pcMap[msg.id].setRemoteDescription(msg.description, msg.type);
+            break;
+        case 'candidate':
+            pcMap[msg.id].addRemoteCandidate(msg.candidate, msg.mid);
+            break;
+
+        default:
+            break;
+    }
+});
+
+function readUserInput() {
+    rl.question('Enter a remote ID to send an offer: ', (peerId) => {
+        if (peerId && peerId.length > 2) {
+            rl.close();
+            console.log('Offering to ', peerId);
+            createPeerConnection(peerId);
+
+            console.log('Creating DataChannel with label "test"');
+            let dc = pcMap[peerId].createDataChannel('test');
+
+            let msgToSend = randomId(MESSAGE_SIZE);
+            let bytesSent = 0;
+            let bytesReceived = 0;
+
+            dc.onOpen(() => {
+                setInterval(() => {
+                    if (dc.bufferedAmount() <= BUFFER_SIZE) {
+                        dc.sendMessage(msgToSend);
+                        bytesSent += msgToSend.length;
+                    }
+                }, 2);
+            });
+
+            dc.onMessage((msg) => {
+                bytesReceived += msg.length;
+            });
+
+            // Report
+            let i = 0;
+            setInterval(() => {
+                console.log(
+                    `${i++}# Sent: ${byte2KB(bytesSent)} KB/s / Received: ${byte2KB(
+                        bytesReceived,
+                    )} KB/s / SendBufferAmount: ${dc.bufferedAmount()} / DataChannelOpen: ${dc.isOpen()}`,
+                );
+                bytesSent = 0;
+                bytesReceived = 0;
+            }, 1000);
+
+            setInterval(() => {
+                console.log(
+                    `Stats# Sent: ${byte2MB(pcMap[peerId].bytesSent())} MB / Received: ${byte2MB(
+                        pcMap[peerId].bytesReceived(),
+                    )} MB / rtt: ${pcMap[peerId].rtt()} ms`,
+                );
+                console.log(`Selected Candidates# ${JSON.stringify(pcMap[peerId].getSelectedCandidatePair())}`);
+                console.log(``);
+            }, 5 * 1000);
+        }
+    });
+}
+
+function createPeerConnection(peerId) {
+    // Create PeerConnection
+    let peerConnection = new nodeDataChannel.PeerConnection('pc', { iceServers: ['stun:stun.l.google.com:19302'] });
+    peerConnection.onStateChange((state) => {
+        console.log('State: ', state);
+    });
+    peerConnection.onGatheringStateChange((state) => {
+        console.log('GatheringState: ', state);
+    });
+    peerConnection.onLocalDescription((description, type) => {
+        ws.send(JSON.stringify({ id: peerId, type, description }));
+    });
+    peerConnection.onLocalCandidate((candidate, mid) => {
+        ws.send(JSON.stringify({ id: peerId, type: 'candidate', candidate, mid }));
+    });
+    peerConnection.onDataChannel((dc) => {
+        rl.close();
+        console.log('DataChannel from ' + peerId + ' received with label "', dc.getLabel() + '"');
+
+        let msgToSend = randomId(MESSAGE_SIZE);
+        let bytesSent = 0;
+        let bytesReceived = 0;
+
+        setInterval(() => {
+            if (dc.bufferedAmount() <= BUFFER_SIZE) {
+                dc.sendMessage(msgToSend);
+                bytesSent += msgToSend.length;
+            }
+        }, 2);
+
+        dc.onMessage((msg) => {
+            bytesReceived += msg.length;
+        });
+
+        // Report
+        let i = 0;
+        setInterval(() => {
+            console.log(
+                `${i++}# Sent: ${byte2KB(bytesSent)} KB/s / Received: ${byte2KB(
+                    bytesReceived,
+                )} KB/s / SendBufferAmount: ${dc.bufferedAmount()} / DataChannelOpen: ${dc.isOpen()}`,
+            );
+            bytesSent = 0;
+            bytesReceived = 0;
+        }, 1000);
+
+        setInterval(() => {
+            console.log(
+                `Stats# Sent: ${byte2MB(pcMap[peerId].bytesSent())} MB / Received: ${byte2MB(
+                    pcMap[peerId].bytesReceived(),
+                )} MB / rtt: ${pcMap[peerId].rtt()} ms`,
+            );
+            console.log(`Selected Candidates# ${JSON.stringify(pcMap[peerId].getSelectedCandidatePair())}`);
+            console.log(``);
+        }, 5 * 1000);
+    });
+
+    pcMap[peerId] = peerConnection;
+}
+
+function randomId(length) {
+    var result = '';
+    var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+    var charactersLength = characters.length;
+    for (var i = 0; i < length; i++) {
+        result += characters.charAt(Math.floor(Math.random() * charactersLength));
+    }
+    return result;
+}
+
+function byte2KB(bytes) {
+    return `${Math.round(bytes / 1024)}`;
+}
+
+function byte2MB(bytes) {
+    return `${Math.round(bytes / (1024 * 1024))}`;
+}

+ 114 - 0
client-server/client.js

@@ -0,0 +1,114 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
+const nodeDataChannel = require('../../lib/index');
+const WebSocket = require('ws');
+const readline = require('readline');
+
+// Init Logger
+nodeDataChannel.initLogger('Error');
+
+// PeerConnection Map
+const pcMap = {};
+
+// Local ID
+const id = randomId(4);
+
+// Signaling Server
+const WS_URL = process.env.WS_URL || 'ws://localhost:8000';
+const ws = new WebSocket(WS_URL + '/' + id, {
+    perMessageDeflate: false,
+});
+
+console.log(`The local ID is: ${id}`);
+console.log(`Waiting for signaling to be connected...`);
+
+ws.on('open', () => {
+    console.log('WebSocket connected, signaling ready');
+    readUserInput();
+});
+
+ws.on('error', (err) => {
+    console.log('WebSocket Error: ', err);
+});
+
+ws.on('message', (msgStr) => {
+    msg = JSON.parse(msgStr);
+    switch (msg.type) {
+        case 'offer':
+            createPeerConnection(msg.id);
+            pcMap[msg.id].setRemoteDescription(msg.description, msg.type);
+            break;
+        case 'answer':
+            pcMap[msg.id].setRemoteDescription(msg.description, msg.type);
+            break;
+        case 'candidate':
+            pcMap[msg.id].addRemoteCandidate(msg.candidate, msg.mid);
+            break;
+
+        default:
+            break;
+    }
+});
+
+function readUserInput() {
+    // Read Line Interface
+    const rl = readline.createInterface({
+        input: process.stdin,
+        output: process.stdout,
+    });
+
+    rl.question('Enter a remote ID to send an offer:\n', (peerId) => {
+        if (peerId && peerId.length > 2) {
+            console.log('Offering to ', peerId);
+            createPeerConnection(peerId);
+
+            console.log('Creating DataChannel with label "test"');
+            let dc = pcMap[peerId].createDataChannel('test');
+            dc.onOpen(() => {
+                dc.sendMessage('Hello from ' + id);
+            });
+
+            dc.onMessage((msg) => {
+                console.log('Message from ' + peerId + ' received:', msg);
+            });
+        }
+
+        rl.close();
+        readUserInput();
+    });
+}
+
+function createPeerConnection(peerId) {
+    // Create PeerConnection
+    let peerConnection = new nodeDataChannel.PeerConnection('pc', { iceServers: ['stun:stun.l.google.com:19302'] });
+    peerConnection.onStateChange((state) => {
+        console.log('State: ', state);
+    });
+    peerConnection.onGatheringStateChange((state) => {
+        console.log('GatheringState: ', state);
+    });
+    peerConnection.onLocalDescription((description, type) => {
+        ws.send(JSON.stringify({ id: peerId, type, description }));
+    });
+    peerConnection.onLocalCandidate((candidate, mid) => {
+        ws.send(JSON.stringify({ id: peerId, type: 'candidate', candidate, mid }));
+    });
+    peerConnection.onDataChannel((dc) => {
+        console.log('DataChannel from ' + peerId + ' received with label "', dc.getLabel() + '"');
+        dc.onMessage((msg) => {
+            console.log('Message from ' + peerId + ' received:', msg);
+        });
+        dc.sendMessage('Hello From ' + id);
+    });
+
+    pcMap[peerId] = peerConnection;
+}
+
+function randomId(length) {
+    var result = '';
+    var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+    var charactersLength = characters.length;
+    for (var i = 0; i < length; i++) {
+        result += characters.charAt(Math.floor(Math.random() * charactersLength));
+    }
+    return result;
+}

+ 265 - 0
client-server/package-lock.json

@@ -0,0 +1,265 @@
+{
+  "name": "client",
+  "version": "1.0.0",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "client",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "ws": "^7.5.3",
+        "yargs": "^16.2.0"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "5.0.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/cliui": {
+      "version": "7.0.4",
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.0",
+        "wrap-ansi": "^7.0.0"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "license": "MIT"
+    },
+    "node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "license": "MIT"
+    },
+    "node_modules/escalade": {
+      "version": "3.1.1",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/get-caller-file": {
+      "version": "2.0.5",
+      "license": "ISC",
+      "engines": {
+        "node": "6.* || 8.* || >= 10.*"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/require-directory": {
+      "version": "2.1.1",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "4.2.2",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "6.0.0",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi": {
+      "version": "7.0.0",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/ws": {
+      "version": "7.5.3",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.3.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": "^5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/y18n": {
+      "version": "5.0.8",
+      "license": "ISC",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/yargs": {
+      "version": "16.2.0",
+      "license": "MIT",
+      "dependencies": {
+        "cliui": "^7.0.2",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.0",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^20.2.2"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/yargs-parser": {
+      "version": "20.2.7",
+      "license": "ISC",
+      "engines": {
+        "node": ">=10"
+      }
+    }
+  },
+  "dependencies": {
+    "ansi-regex": {
+      "version": "5.0.0"
+    },
+    "ansi-styles": {
+      "version": "4.3.0",
+      "requires": {
+        "color-convert": "^2.0.1"
+      }
+    },
+    "cliui": {
+      "version": "7.0.4",
+      "requires": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.0",
+        "wrap-ansi": "^7.0.0"
+      }
+    },
+    "color-convert": {
+      "version": "2.0.1",
+      "requires": {
+        "color-name": "~1.1.4"
+      }
+    },
+    "color-name": {
+      "version": "1.1.4"
+    },
+    "emoji-regex": {
+      "version": "8.0.0"
+    },
+    "escalade": {
+      "version": "3.1.1"
+    },
+    "get-caller-file": {
+      "version": "2.0.5"
+    },
+    "is-fullwidth-code-point": {
+      "version": "3.0.0"
+    },
+    "require-directory": {
+      "version": "2.1.1"
+    },
+    "string-width": {
+      "version": "4.2.2",
+      "requires": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.0"
+      }
+    },
+    "strip-ansi": {
+      "version": "6.0.0",
+      "requires": {
+        "ansi-regex": "^5.0.0"
+      }
+    },
+    "wrap-ansi": {
+      "version": "7.0.0",
+      "requires": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      }
+    },
+    "ws": {
+      "version": "7.5.3",
+      "requires": {}
+    },
+    "y18n": {
+      "version": "5.0.8"
+    },
+    "yargs": {
+      "version": "16.2.0",
+      "requires": {
+        "cliui": "^7.0.2",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.0",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^20.2.2"
+      }
+    },
+    "yargs-parser": {
+      "version": "20.2.7"
+    }
+  }
+}

+ 16 - 0
client-server/package.json

@@ -0,0 +1,16 @@
+{
+  "name": "client",
+  "version": "1.0.0",
+  "description": "",
+  "main": "client.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "ws": "^7.5.3",
+    "yargs": "^16.2.0"
+  }
+}

+ 29 - 0
client-server/signaling-server.js

@@ -0,0 +1,29 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
+const WebSocket = require('ws');
+
+const clients = {};
+
+const wss = new WebSocket.Server({ port: 8000 });
+
+wss.on('connection', (ws, req) => {
+    const id = req.url.replace('/', '');
+    console.log(`New Connection from ${id}`);
+
+    clients[id] = ws;
+    ws.on('message', (buffer) => {
+        let msg = JSON.parse(buffer);
+        let peerId = msg.id;
+        let peerWs = clients[peerId];
+
+        console.log(`Message from ${id} to ${peerId} : ${buffer}`);
+        if (!peerWs) return console.error(`Can not find peer with ID ${peerId}`);
+
+        msg.id = id;
+        peerWs.send(JSON.stringify(msg));
+    });
+
+    ws.on('close', () => {
+        console.log(`${id} disconected`);
+        delete clients[id];
+    });
+});

+ 18 - 0
media-send/README.md

@@ -0,0 +1,18 @@
+# Examples
+* Prepare a signaling server by yourself
+
+# peer-send
+* This example uses raspi 4b 8G.
+* Install GStreamer yourself.
+
+## How to Install?
+* sudo apt-get install libx264-dev libjpeg-dev
+* sudo apt-get install libgstreamer1.0-dev
+* sudo apt-get install libgstreamer-plugins-base1.0-dev
+* sudo apt-get install libgstreamer-plugins-bad1.0-dev;
+* sudo apt-get install gstreamer1.0-plugins-ugly
+* sudo apt-get install gstreamer1.0-tools
+* sudo apt-get install gstreamer1.0-gl
+* sudo apt-get install gstreamer1.0-gtk3
+* sudo apt-get install gstreamer1.0-pulseaudio
+  > You can also see [Install GStreamer 1.18 on Raspberry Pi 4.](https://qengineering.eu/install-gstreamer-1.18-on-raspberry-pi-4.html)

+ 24 - 0
media-send/peer-recv/.gitignore

@@ -0,0 +1,24 @@
+.DS_Store
+node_modules
+/dist
+
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+yarn.lock

+ 5 - 0
media-send/peer-recv/README.md

@@ -0,0 +1,5 @@
+## How to Use?
+* Init Project
+  * yarn 
+* Start server;
+  * yarn dev 

+ 20 - 0
media-send/peer-recv/package.json

@@ -0,0 +1,20 @@
+{
+  "name": "test",
+  "version": "0.1.0",
+  "private": true,
+  "author": "Caner",
+  "email": "5658514@qq.com",
+  "scripts": {
+    "dev": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "lint": "vue-cli-service lint"
+  },
+  "dependencies": {
+    "socket.io-client": "^4.4.1",
+    "vue": "^2.6.11"
+  },
+  "devDependencies": {
+    "@vue/cli-service": "~4.5.0",
+    "vue-template-compiler": "^2.6.11"
+  }
+}

+ 17 - 0
media-send/peer-recv/public/index.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
+    <meta name="viewport" content="initial-scale=1,maximum-scale=1, minimum-scale=1, user-scalable=no">
+    <title>test media</title>
+  </head>
+  <body>
+    <noscript>
+      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+    </noscript>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+  </body>
+</html>

+ 94 - 0
media-send/peer-recv/src/App.vue

@@ -0,0 +1,94 @@
+<template>
+  <div id="app">
+    <video id="v2" autoplay playsinline muted></video>
+  </div>
+</template>
+<script>
+const { io } = require("socket.io-client");
+export default {
+  data() {
+    return {
+      socket: null,
+      HOST: "", //Your signaling server address
+      Peer: null
+    };
+  },
+  methods: {
+    intSoketRtc(host) {
+      if(!host)  return alert('please change socket address') 
+      // int socket
+      this.socket = io(host, {
+        auth: {
+          roomID: "test",
+          name: "123",
+        },
+        transports: ["websocket"],
+      });
+
+      // socket
+      this.socket.on("connect", () => {
+        this.Peer = new RTCPeerConnection({
+          bundlePolicy: "max-bundle",
+        });
+
+        // listen state
+        this.Peer.onicegatheringstatechange = () => {
+          console.log("GatheringState: ", this.Peer.iceGatheringState);
+          if (this.Peer.iceGatheringState === "complete") {
+            const answer = this.Peer.localDescription;
+            this.socket.emit("msg", answer);
+          }
+        };
+
+        // listen track
+        this.Peer.ontrack = (evt) => {
+          console.log("track", evt.streams[0]);
+          const video = document.getElementById("v2");
+          video.srcObject = evt.streams[0];
+        };
+
+        console.log("connected");
+      });
+
+      // listen data
+      this.socket.on("msg", async (data) => {
+        console.log(data);
+        if (data.type == "offer") {
+          await this.Peer.setRemoteDescription(data);
+          const answer = await this.Peer.createAnswer();
+          await this.Peer.setLocalDescription(answer);
+        }
+      });
+
+      // listen join
+      this.socket.on("joined", async (user) => {
+        console.log(`${user.name}_${user.ip}_joined_${user.roomID}`);
+        this.socket.emit("msg", { type: "startRTC" });
+      });
+
+      // listen leave
+      this.socket.on("leaved", (user) => {
+        console.log(`${user.name}_${user.ip}_leaved_${user.roomID}`);
+      });
+
+      // listen error
+      this.socket.on("connect_error", (err) => {
+        console.log("connect_error", err);
+      });
+    },
+  },
+  mounted() {
+    this.intSoketRtc(this.HOST);
+  },
+};
+</script>
+<style scoped>
+video {
+  width: 500px;
+  height: 500px;
+  background: none;
+  object-fit: fill;
+  border: solid 1px red;
+  margin: 0 auto;
+}
+</style>

+ 6 - 0
media-send/peer-recv/src/main.js

@@ -0,0 +1,6 @@
+import Vue from 'vue'
+import App from './App.vue'
+Vue.config.productionTip = false
+new Vue({
+  render: h => h(App),
+}).$mount('#app')

+ 12 - 0
media-send/peer-recv/vue.config.js

@@ -0,0 +1,12 @@
+module.exports = {
+    publicPath: './',
+    outputDir: '../www',
+    assetsDir: 'static',
+    productionSourceMap: false,
+    lintOnSave: true,
+    filenameHashing: true,
+    devServer: {
+        https: false,
+        port: 4562
+    }
+}

+ 30 - 0
media-send/peer-send/.gitignore

@@ -0,0 +1,30 @@
+# ---> Node
+# Logs
+logs
+*.log
+npm-debug.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directory
+# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
+node_modules
+gitHub

+ 8 - 0
media-send/peer-send/LICENSE

@@ -0,0 +1,8 @@
+MIT License
+Copyright (c) <year> <copyright holders>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 5 - 0
media-send/peer-send/README.md

@@ -0,0 +1,5 @@
+## How to Use?
+* Init Project
+  * yarn
+* Start server;
+  * node index

+ 103 - 0
media-send/peer-send/index.js

@@ -0,0 +1,103 @@
+const io = require("socket.io-client")
+const { PeerConnection, Video } = require('node-datachannel');
+const { createSocket } = require('dgram')
+const { spawn } = require('child_process')
+const HOST = '' //Your signaling server address
+class CarServer {
+    constructor() {
+        this.Peer = null
+        this.track = null
+        this.child = null
+        this.udp = null
+        if (!HOST) console.error('Enter the signaling server address');
+        this.socket = io(HOST, {
+            auth: {
+                roomID: 'test',
+                name: '456'
+            }
+        });
+        this.socket.on('connect', this.connected.bind(this))
+        this.socket.on('msg', this.onMsg.bind(this))
+        this.socket.on('leaved', this.onLeved.bind(this))
+        this.socket.on('joined', this.startVideo.bind(this))
+    }
+    // socket conect
+    connected() {
+        console.log('connected');
+        this.Peer = new PeerConnection("Peer1", { iceServers: [] });
+        // send offer
+        this.Peer.onGatheringStateChange(state => {
+            console.log('GatheringState: ', state);
+            if (state === 'complete') {
+                const offer = this.Peer.localDescription();
+                this.socket.emit("msg", offer);
+            }
+        })
+    }
+
+    // listen data
+    onMsg(data) {
+        try {
+            if (data.type == 'answer') {
+                this.Peer.setRemoteDescription(data.sdp, data.type);
+            } else if (data.type === 'startRTC') {
+                this.startVideo()
+            }
+        } catch (error) {
+            console.log('msg error:', error);
+        }
+    }
+
+    // listen leave
+    onLeved() {
+        if (this.child) this.child.kill()
+        if (this.udp) this.udp.close()
+        this.child = null
+        this.udp = null
+        process.exit(1)
+    }
+
+    // start video
+    startVideo() {
+        try {
+            if (this.child) this.child.kill()
+            if (this.udp) this.udp.close()
+
+            // test video
+            const video = new Video('video', 'SendOnly')
+            video.addH264Codec(96)
+            video.addSSRC(42, "video-send")
+            this.track = this.Peer.addTrack(video)
+            this.Peer.setLocalDescription()
+
+            // UDP server
+            const port = 7788
+            this.udp = createSocket("udp4")
+            this.udp.bind(port)
+
+            // video push
+            const args = [
+                "libcamerasrc",
+                "video/x-raw,width=320,height=240",
+                "videoconvert",
+                "queue",
+                "x264enc tune=zerolatency bitrate=1000 key-int-max=30",
+                "video/x-h264, profile=constrained-baseline",
+                "rtph264pay pt=96 mtu=1200 ssrc=42",
+                `udpsink host=127.0.0.1 port=${port}`,
+            ].join(" ! ").split(" ")
+            this.child = spawn("gst-launch-1.0", args)
+
+            // listen UDP
+            this.udp.on("message", (data) => {
+                if (!this.track.isOpen()) return
+                this.track.sendMessageBinary(data)
+            });
+
+        } catch (error) {
+            console.log('startvideo:', error)
+        }
+    }
+
+}
+new CarServer()

+ 21 - 0
media-send/peer-send/package.json

@@ -0,0 +1,21 @@
+{
+  "name": "client",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "author": "Caner",
+  "email": "5658514@qq.com",
+  "scripts": {
+    "build": "pkg . -t node14-linux-x64 --out-path=dist/",
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "bin": "./index.js",
+  "pkg": {
+    "scripts": "lib/*.js"
+  },
+  "license": "ISC",
+  "dependencies": {
+    "node-datachannel": "^0.3.4",
+    "socket.io-client": "^4.4.1"
+  }
+}

+ 319 - 0
media-send/peer-send/yarn.lock

@@ -0,0 +1,319 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@socket.io/component-emitter@~3.1.0":
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
+  integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
+
+base64-js@^1.3.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
+bl@^4.0.3:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
+  integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
+  dependencies:
+    buffer "^5.5.0"
+    inherits "^2.0.4"
+    readable-stream "^3.4.0"
+
+buffer@^5.5.0:
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
+  integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
+  dependencies:
+    base64-js "^1.3.1"
+    ieee754 "^1.1.13"
+
+chownr@^1.1.1:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
+  integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
+
+debug@~4.3.1, debug@~4.3.2:
+  version "4.3.4"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
+  integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
+  dependencies:
+    ms "2.1.2"
+
+decompress-response@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
+  integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==
+  dependencies:
+    mimic-response "^3.1.0"
+
+deep-extend@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
+  integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
+
+detect-libc@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd"
+  integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==
+
+end-of-stream@^1.1.0, end-of-stream@^1.4.1:
+  version "1.4.4"
+  resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
+  integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
+  dependencies:
+    once "^1.4.0"
+
+engine.io-client@~6.2.1:
+  version "6.2.2"
+  resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.2.2.tgz#c6c5243167f5943dcd9c4abee1bfc634aa2cbdd0"
+  integrity sha512-8ZQmx0LQGRTYkHuogVZuGSpDqYZtCM/nv8zQ68VZ+JkOpazJ7ICdsSpaO6iXwvaU30oFg5QJOJWj8zWqhbKjkQ==
+  dependencies:
+    "@socket.io/component-emitter" "~3.1.0"
+    debug "~4.3.1"
+    engine.io-parser "~5.0.3"
+    ws "~8.2.3"
+    xmlhttprequest-ssl "~2.0.0"
+
+engine.io-parser@~5.0.3:
+  version "5.0.4"
+  resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.4.tgz#0b13f704fa9271b3ec4f33112410d8f3f41d0fc0"
+  integrity sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==
+
+expand-template@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
+  integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
+
+fs-constants@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
+  integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
+
+github-from-package@0.0.0:
+  version "0.0.0"
+  resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
+  integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==
+
+ieee754@^1.1.13:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
+  integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
+
+inherits@^2.0.3, inherits@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+ini@~1.3.0:
+  version "1.3.8"
+  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
+  integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
+
+lru-cache@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
+  integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
+  dependencies:
+    yallist "^4.0.0"
+
+mimic-response@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
+  integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
+
+minimist@^1.2.0, minimist@^1.2.3:
+  version "1.2.6"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
+  integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
+
+mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
+  version "0.5.3"
+  resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
+  integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
+
+ms@2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+napi-build-utils@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"
+  integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==
+
+node-abi@^3.3.0:
+  version "3.22.0"
+  resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.22.0.tgz#00b8250e86a0816576258227edbce7bbe0039362"
+  integrity sha512-u4uAs/4Zzmp/jjsD9cyFYDXeISfUWaAVWshPmDZOFOv4Xl4SbzTXm53I04C2uRueYJ+0t5PEtLH/owbn2Npf/w==
+  dependencies:
+    semver "^7.3.5"
+
+node-datachannel@^0.3.4:
+  version "0.3.4"
+  resolved "https://registry.yarnpkg.com/node-datachannel/-/node-datachannel-0.3.4.tgz#526fd245c3fdf9bcdcabb0c78ede3aa89531d67a"
+  integrity sha512-iIEUQNVmJ+N3KTpOoe3I+hhdl8lwdPjIT9019tipJ3YP6R0/rDlqh7yLtI5547aomANXV86XUUun/spv4FuHQQ==
+  dependencies:
+    prebuild-install "^7.0.1"
+
+once@^1.3.1, once@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+  integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
+  dependencies:
+    wrappy "1"
+
+prebuild-install@^7.0.1:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45"
+  integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==
+  dependencies:
+    detect-libc "^2.0.0"
+    expand-template "^2.0.3"
+    github-from-package "0.0.0"
+    minimist "^1.2.3"
+    mkdirp-classic "^0.5.3"
+    napi-build-utils "^1.0.1"
+    node-abi "^3.3.0"
+    pump "^3.0.0"
+    rc "^1.2.7"
+    simple-get "^4.0.0"
+    tar-fs "^2.0.0"
+    tunnel-agent "^0.6.0"
+
+pump@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
+  integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
+  dependencies:
+    end-of-stream "^1.1.0"
+    once "^1.3.1"
+
+rc@^1.2.7:
+  version "1.2.8"
+  resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
+  integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
+  dependencies:
+    deep-extend "^0.6.0"
+    ini "~1.3.0"
+    minimist "^1.2.0"
+    strip-json-comments "~2.0.1"
+
+readable-stream@^3.1.1, readable-stream@^3.4.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
+  integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
+  dependencies:
+    inherits "^2.0.3"
+    string_decoder "^1.1.1"
+    util-deprecate "^1.0.1"
+
+safe-buffer@^5.0.1, safe-buffer@~5.2.0:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+semver@^7.3.5:
+  version "7.3.7"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
+  integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==
+  dependencies:
+    lru-cache "^6.0.0"
+
+simple-concat@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
+  integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==
+
+simple-get@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543"
+  integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==
+  dependencies:
+    decompress-response "^6.0.0"
+    once "^1.3.1"
+    simple-concat "^1.0.0"
+
+socket.io-client@^4.4.1:
+  version "4.5.1"
+  resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.5.1.tgz#cab8da71976a300d3090414e28c2203a47884d84"
+  integrity sha512-e6nLVgiRYatS+AHXnOnGi4ocOpubvOUCGhyWw8v+/FxW8saHkinG6Dfhi9TU0Kt/8mwJIAASxvw6eujQmjdZVA==
+  dependencies:
+    "@socket.io/component-emitter" "~3.1.0"
+    debug "~4.3.2"
+    engine.io-client "~6.2.1"
+    socket.io-parser "~4.2.0"
+
+socket.io-parser@~4.2.0:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.1.tgz#01c96efa11ded938dcb21cbe590c26af5eff65e5"
+  integrity sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==
+  dependencies:
+    "@socket.io/component-emitter" "~3.1.0"
+    debug "~4.3.1"
+
+string_decoder@^1.1.1:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
+  integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+  dependencies:
+    safe-buffer "~5.2.0"
+
+strip-json-comments@~2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
+  integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
+
+tar-fs@^2.0.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
+  integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
+  dependencies:
+    chownr "^1.1.1"
+    mkdirp-classic "^0.5.2"
+    pump "^3.0.0"
+    tar-stream "^2.1.4"
+
+tar-stream@^2.1.4:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
+  integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
+  dependencies:
+    bl "^4.0.3"
+    end-of-stream "^1.4.1"
+    fs-constants "^1.0.0"
+    inherits "^2.0.3"
+    readable-stream "^3.1.1"
+
+tunnel-agent@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+  integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==
+  dependencies:
+    safe-buffer "^5.0.1"
+
+util-deprecate@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+  integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
+
+wrappy@1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+  integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
+
+ws@~8.2.3:
+  version "8.2.3"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
+  integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
+
+xmlhttprequest-ssl@~2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67"
+  integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==
+
+yallist@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+  integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==

+ 22 - 0
media/README.md

@@ -0,0 +1,22 @@
+# Examples
+
+# media
+
+## Example Webcam from Browser to Port 5000
+This is an example copy/paste demo to send your webcam from your browser and out port 5000 through the demo application.
+
+## How to use
+Open main.html in your browser (you must open it either as HTTPS or as a domain of http://localhost).
+
+Start the application and copy it's offer into the text box of the web page.
+
+Copy the answer of the webpage back into the application.
+
+You will now see RTP traffic on `localhost:5000` of the computer that the application is running on.
+
+Use the following gstreamer demo pipeline to display the traffic
+(you might need to wave your hand in front of your camera to force an I-frame).
+
+```
+$ gst-launch-1.0 udpsrc address=127.0.0.1 port=5000 caps="application/x-rtp" ! queue ! rtph264depay ! video/x-h264,stream-format=byte-stream ! queue ! avdec_h264 ! queue ! autovideosink
+```

+ 45 - 0
media/main.html

@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>libdatachannel media example</title>
+</head>
+<body>
+
+<p>Please enter the offer provided to you by the application: </p>
+<textarea cols="50" rows="50"></textarea>
+<button>Submit</button>
+
+<script>
+    document.querySelector('button').addEventListener('click',  async () => {
+        let offer = JSON.parse(document.querySelector('textarea').value);
+        rtc = new RTCPeerConnection({
+            // Recommended for libdatachannel
+            bundlePolicy: "max-bundle",
+        });
+
+        rtc.onicegatheringstatechange = (state) => {
+            if (rtc.iceGatheringState === 'complete') {
+                // We only want to provide an answer once all of our candidates have been added to the SDP.
+                let answer = rtc.localDescription;
+                document.querySelector('textarea').value = JSON.stringify({"type": answer.type, sdp: answer.sdp});
+                document.querySelector('p').value = 'Please paste the answer in the application.';
+                alert('Please paste the answer in the application.');
+            }
+        }
+        await rtc.setRemoteDescription(offer);
+
+        let media = await navigator.mediaDevices.getUserMedia({
+            video: {
+                width: 1280,
+                height: 720
+            }
+        });
+        media.getTracks().forEach(track => rtc.addTrack(track, media));
+        let answer = await rtc.createAnswer();
+        await rtc.setLocalDescription(answer);
+    })
+</script>
+
+</body>
+</html>

+ 55 - 0
media/media.js

@@ -0,0 +1,55 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
+const nodeDataChannel = require('../../lib/index');
+const readline = require('readline');
+var dgram = require('dgram');
+
+var client = dgram.createSocket('udp4');
+
+// Read Line Interface
+const rl = readline.createInterface({
+    input: process.stdin,
+    output: process.stdout,
+});
+
+// Init Logger
+nodeDataChannel.initLogger('Debug');
+
+let peerConnection = new nodeDataChannel.PeerConnection('pc', { iceServers: [] });
+
+peerConnection.onStateChange((state) => {
+    console.log('State: ', state);
+});
+peerConnection.onGatheringStateChange((state) => {
+    // console.log('GatheringState: ', state);
+
+    if (state == 'complete') {
+        let desc = peerConnection.localDescription();
+        console.log('');
+        console.log('## Please copy the offer below to the web page:');
+        console.log(JSON.stringify(desc));
+        console.log('\n\n');
+        console.log('## Expect RTP video traffic on localhost:5000');
+        rl.question('## Please copy/paste the answer provided by the browser: \n', (sdp) => {
+            let sdpObj = JSON.parse(sdp);
+            peerConnection.setRemoteDescription(sdpObj.sdp, sdpObj.type);
+            console.log(track.isOpen());
+            rl.close();
+        });
+    }
+});
+
+let video = new nodeDataChannel.Video('video', 'RecvOnly');
+video.addH264Codec(96);
+video.setBitrate(3000);
+
+let track = peerConnection.addTrack(video);
+let session = new nodeDataChannel.RtcpReceivingSession();
+
+track.setMediaHandler(session);
+track.onMessage((msg) => {
+    client.send(msg, 5000, '127.0.0.1', (err, n) => {
+        if (err) console.log(err, n);
+    });
+});
+
+peerConnection.setLocalDescription();