preface

I previously wrote a [head to toe] masturbate a multi-person video chat – front-end WebRTC combat (A), mainly about some of the basic knowledge of WebRTC and the simple implementation of a single call. The original plan is to write about the multi-person phone call. In view of the message that some students want to see the drawing board, I put this article in advance, hoping to provide some ideas for everyone.

RTCDataChannel RTCDataChannel RTCDataChannel RTCDataChannel RTCDataChannel

Special note: since this implementation is based on the knowledge points and related examples from the previous period, it is strongly recommended that students who are not familiar with the basics of WebRTC watch the portal together with the previous part. Relevant examples of recent articles are grouped together in a single project, as of this issue listed below:

  • This article’s sample source library webrtC-Stream
  • The article warehouse 🍹 🍰 fe – code
  • This article demo address (suggested Google view)

As usual, let’s take a look at our practical goal for this issue: to create a drawing board that allows two people (based on the 1-to-1 peer-to-peer connection from the previous article) to draw collaboratively. What is the concept? In simple terms, two people can share a drawing board and both can draw on it.

Feel the fear first! Trembling!!!! Human!(The picture shows a whiteboard demonstration, shared below)

RTCDataChannel

Let’s finish up what we learned last time, because chestnuts are going to use it today.

introduce

To put it simply, RTCDataChannel is to establish a two-way data channel in the point-to-point connection, so as to obtain the point-to-point transmission capability of data such as text and files. It relies on the Flow Control Transport Protocol (SCTP), a transport protocol similar to TCP and UDP that can run directly on top of the IP protocol. However, in the case of WebRTC, SCTP is tunnelled over a secure DTLS tunnel, which itself runs over UDP. Well, I am a poor student, for this paragraph I can only say, read! You can view the original text directly.

In addition, RTCDataChannel is similar to WebSocket in general, except that WebSocket is not a P2P connection and requires the server to do the transfer.

use

The RTCDataChannel is created using the RTCPeerConnection described in the previous installment.

/ / create
let Channel = RTCPeerConnection.createDataChannel('messagechannel', options);
// Messagechannel can be thought of as an alias for DataChannel. The limit is 65,535 bytes.
// options can set some properties, generally the default is good.

/ / receive
RTCPeerConnection.ondatachannel = function(event) {
  let channel = event.channel;
}
Copy the code

RTCDataChannel creates an instance using createDataChannel on one end and onDatachannel on the other end. One important thing to note, however, is that it must be the caller that created createOffer to create the channel createDataChannel.

RTCDataChannel some properties, more can see MDN

  • Label: Alias name mentioned at creation time.
  • Ordered: Indicates whether sent messages need to arrive at their destination in the order in which they were sent (true), or whether they are allowed to arrive unordered (false). Default value: true.
  • BinaryType: is a DOMString that represents the type of binary data to be sent. The value is blob or ArrayBuffer. The default value is “blob”.
  • ReadyState: indicates the state of the data connection:
    • Connecting A connection is also created in the initial state.
    • The open connection is successful and running.
    • Closing a connection closes, no new send tasks are accepted, but messages in the buffered queue continue to be sent or received. So if you don’t finish sending it, you keep sending it.
    • The closed connection is completely closed.

RTCDataChannel is very similar to WebSocket. It is very similar to WebSocket. Let’s take a look at the usage based on the local 1-to-1 connection from last time.

Here again, the series of articles is a bit more trouble, a lot of content behind are based on the basis of the previous, but many students have not read the previous article. However, I cannot repeat the previous content every time, so I strongly recommend students who have the need to read the portal together with the previous article, I hope you understand.

RTCDataChannel (RTCDataChannel); RTCDataChannel (RTCDataChannel); RTCDataChannel (RTCDataChannel);

// this.peerB RTCPeerConnection instance of the caller
this.channelB = this.peerB.createDataChannel('messagechannel'); / / create the Channel
this.channelB.onopen = (event) = > { // The listening connection succeeded
    console.log('channelB onopen', event);
    this.messageOpen = true; // A message box is displayed after the connection is successful
};
this.channelB.onclose = function(event) { // The listening connection is closed
    console.log('channelB onclose', event);
};

// Send a message
send() {
    this.channelB.send(this.sendText);
    this.sendText = ' ';
}
Copy the code
// this.peerA receiving RTCPeerConnection instance
this.peerA.ondatachannel = (event) = > {
    this.channelA = event.channel; // Get the receiving channel instance
    this.channelA.onopen = (e) = > { // The listening connection succeeded
        console.log('channelA onopen', e);
    };
    this.channelA.onclose = (e) = > { // The listening connection is closed
        console.log('channelA onclose', e);
    };
    this.channelA.onmessage = (e) = > { // Listen for message reception
        this.receiveText = e.data; // The receiver box displays a message
        console.log('channelA onmessage', e.data);
    };
};
Copy the code

The process of establishing a peer-to-peer connection is omitted here, and these two pieces of code allow for simple text transfer.

The whiteboard demonstrated

demand

Ok, that’s the end of WebRTC’s three apis, so let’s move on to our first practical demonstration of the day – whiteboard. For those of you who don’t know much about whiteboard presentation, generally speaking, it means that you can write and draw on the whiteboard in real time for each other to see. Take a look at my masterpiece:

Well, as shown above, whiteboard operations are shown in real time in the demo screen. It’s actually pretty easy to do a whiteboard presentation based on WebRTC, because we don’t need video calls, so we don’t need to get local media streams. Then we can directly use the Canvas Canvas as a media stream to establish a connection, so that the other party can see your painting. To turn a Canvas into a media stream, we use a magic API called captureStream.

this.localstream = this.$refs['canvas'].captureStream();
Copy the code

One sentence can turn Canvas into media stream, so the demo screen is still the video tag playing media stream, but this time it is not the stream obtained from the camera, but converted by Canvas.

Encapsulation Canvas class

Now that we have point-to-point connections, and we have whiteboard flow, we just don’t have a Canvas to draw on. So quick, look, Canvas is here. The source address

  • The function point

From the diagram we can see what functions this artboard class needs: Draw circle, Draw line, Draw rectangle, Draw polygon, eraser, Retreat, advance, clear screen, line width, color, these are optional functions.

Further analysis:

  1. Draw a variety of shapes, must use the mouse event, to record the mouse movement position to draw;
  2. To draw a polygon, the user needs to choose which side shape is in the end, at least 3 sides, that is, triangles;
  3. Line width and color are also things that users can change, so we need to provide an interface to modify these properties;
  4. Retracting and advancing, which means we need to save the image each time we draw it, save it when the mouse is up; And retractions and advances are not unlimited, there are boundary points;
  5. Think about it: when you draw 5 steps, now you go back to step 3 and want to draw again, should you remove steps 4 and 5? If not, how far does the newly drawn step count?

To sum up, we can first list the general framework.

// Palette.js
class Palette {
    constructor(){}gatherImage() { // Collect images
    }
    reSetImage() { // Reset to the previous frame
    }
    onmousedown(e) { // Mouse down
    }
    onmousemove(e) { // Mouse movement
    }
    onmouseup() { // Mouse up
    }
    line() { // Draw a linear
    }
    rect() { // Draw a rectangle
    }
    polygon() { // Draw a polygon
    }
    arc() { // Draw a circle
    }
    eraser() { / / eraser
    }
    cancel() { / / withdraw
    }
    go () { / / to go forward
    }
    clear() { / / clear screen
    }
    changeWay() { // Change the drawing conditions
    }
    destroy() { / / destroy}}Copy the code
  • Draw lines

Any drawing, all need to go through the mouse down, mouse movement, mouse lift these steps;

onmousedown(e) { // Mouse down
    this.isClickCanvas = true; // Mouse down the icon
    this.x = e.offsetX; // Get the coordinates of the mouse down
    this.y = e.offsetY;
    this.last = [this.x, this.y]; // Save the coordinates each time
    this.canvas.addEventListener('mousemove'.this.bindMousemove); // Listen for mouse movement events
}
onmousemove(e) { // Mouse movement
    this.isMoveCanvas = true; // Mouse moves the logo
    let endx = e.offsetX;
    let endy = e.offsetY;
    let width = endx - this.x;
    let height = endy - this.y;
    let now = [endx, endy]; // The coordinate to which the current move is made
    switch (this.drawType) {
        case 'line' :
            this.line(this.last, now, this.lineWidth, this.drawColor); // The method of drawing lines
            break; }}onmouseup() { // Mouse up
    if (this.isClickCanvas) {
        this.isClickCanvas = false;
        this.canvas.removeEventListener('mousemove'.this.bindMousemove); // Remove the mouse movement event
        if (this.isMoveCanvas) { // The mouse will not be saved without moving
            this.isMoveCanvas = false;
            this.gatherImage(); // Save the image each time}}}Copy the code

BindMousemove. This is because we need to bind this, but bind does not return the same function each time, and the remove event cannot be removed if it is not bound to the same function. So you need to use a variable to hold the bind function.

this.bindMousemove = this.onmousemove.bind(this); // EventListener cannot be bind
this.bindMousedown = this.onmousedown.bind(this);
this.bindMouseup = this.onmouseup.bind(this);
Copy the code

In the this.line method, we pass all the parameters as function parameters so that each step of drawing each other’s drawing needs to be synchronized when sharing the artboard. When drawing lines, adopt the way of connecting coordinate points of each movement into lines, so that the drawing is more continuous. If the point is drawn directly, large faults will appear if the speed is too fast.

line(last, now, lineWidth, drawColor) { // Draw a linear
    this.paint.beginPath();
    this.paint.lineCap = "round"; // Set the style of the line and the indirect line
    this.paint.lineJoin = "round";
    this.paint.lineWidth = lineWidth;
    this.paint.strokeStyle = drawColor;
    this.paint.moveTo(last[0], last[1]);
    this.paint.lineTo(now[0], now[1]);
    this.paint.closePath();
    this.paint.stroke(); // Draw
    this.last = now; // Update the last coordinates
}
Copy the code
  • Withdraw, advance

When the mouse is raised, a gatherImage method is used to gather images, which is also the key to recall and advance.

gatherImage() { // Collect images
    this.imgData = this.imgData.slice(0.this.index + 1);
    // Intercepts stored IMGData to index each time the mouse is raised
    let imgData = this.paint.getImageData(0.0.this.width, this.height);
    this.imgData.push(imgData);
    this.index = this.imgData.length - 1; // Reset index to the last bit of imgData after saving
}
Copy the code

Recall one of the problems mentioned earlier, when we retreat to a certain step and start painting from that step, we need to delete all subsequent images from that step to avoid confusion. So we use a global index as the identifier of the current drawing frame of the image. When saving the image each time, we intercept the image cache array imgData to keep consistent with the index, and reset the index to the last bit after saving.

cancel() { / / withdraw
    if (--this.index <0) { // Reset to 0 bits at most
        this.index = 0;
        return;
    }
    this.paint.putImageData(this.imgData[this.index], 0.0); / / to draw
}
go () { / / to go forward
    if(+ +this.index > this.imgData.length -1) { // Up to length-1
        this.index = this.imgData.length -1;
        return;
    }
    this.paint.putImageData(this.imgData[this.index], 0.0);
}
Copy the code
  • The eraser

For eraser we use a Canvas property, Clip. Simply put, you draw a clipped region of the image, and subsequent operations will only scope that region. So when we set the clipping area to a small dot, then even if we clear the entire artboard, we only clear the area of the dot. When you’re done, restore it.

eraser(endx, endy, width, height, lineWidth) { / / eraser
    this.paint.save(); // Cache before cutting
    this.paint.beginPath();
    this.paint.arc(endx, endy, lineWidth / 2.0.2 * Math.PI);
    this.paint.closePath();
    this.paint.clip(); / / cutting
    this.paint.clearRect(0.0, width, height);
    this.paint.fillStyle = '#fff';
    this.paint.fillRect(0.0, width, height);
    this.paint.restore(); / / reduction
}
Copy the code
  • rectangular

In drawing a rectangle, for example, because it is not a continuous action, it should be drawn with the coordinates of the last position of the mouse. At this point, you should keep clearing the artboard and resetting it to the previous frame.

If you look at the phenomenon of not doing a reset, it should be easier to understand. Here are the moments of wonder:

rect(x, y, width, height, lineWidth, drawColor) { // Draw a rectangle
    this.reSetImage();
    this.paint.lineWidth = lineWidth;
    this.paint.strokeStyle = drawColor;
    this.paint.strokeRect(x, y, width, height);
}
reSetImage() { // Reset to the previous frame
    this.paint.clearRect(0.0.this.width, this.height);
    if(this.imgData.length >= 1) {this.paint.putImageData(this.imgData[this.index], 0.0); }}Copy the code

So much for Canvas encapsulation, since the rest of the basic functions are similar, there are a few minor changes to making shared artboards, which we will cover later. The source code is here

Establish a connection

Now that we’re all set, it’s time for the peer connection to open. Instead of getting the media stream, we’ll use the Canvas stream instead.

async createMedia() {
    // Save the canvas stream globally
    this.localstream = this.$refs['canvas'].captureStream();
    this.initPeer(); // After obtaining the media stream, call the function to initialize RTCPeerConnection
}
Copy the code

The rest of the work is exactly the same as our previous 1 V 1 local connection, no longer paste here, students need to view the previous article or directly view the source code.

A Shared sketchpad

demand

So much preparation, everything is for today’s ultimate goal, the completion of a collaborative shared art board. In fact, we’ve covered all the points that we need to use in shared artboards. We’re going to make some changes based on the 1 V 1 network connection from last time, so let’s go back to the diagram in the introduction.

Taking a closer look at where I circled, you can see from the log-in that this is a screenshot of the page I opened in both browsers. Of course, you can go to the online address and actually do it. Two pages, two artboards, two people can operate, their operations will be synchronized to each other’s artboards. On the right is a simple chat room, all data synchronization and chat messages are based on today’s RTCDataChannel.

Establish a connection

This time, no video stream is required, and no Canvas stream is required, so we set up the data channel directly during the point-to-point connection.

createDataChannel() { / / create DataChannel
    try{
        this.channel = this.peer.createDataChannel('messagechannel');
        this.handleChannel(this.channel);
    } catch (e) {
        console.log('createDataChannel:', e); }},onDataChannel() { / / receive DataChannel
    this.peer.ondatachannel = (event) = > {
        // console.log('ondatachannel', event);
        this.channel = event.channel;
        this.handleChannel(this.channel);
    };
},
handleChannel(channel) { / / processing channel
    channel.binaryType = 'arraybuffer';
    channel.onopen = (event) = > { // The connection succeeded
        console.log('channel onopen', event);
        this.isToPeer = true; // The connection succeeded
        this.loading = false; / / terminate the loading
        this.initPalette();
    };
    channel.onclose = function(event) { // The connection is closed
        console.log('channel onclose', event)
    };
    channel.onmessage = (e) = > { // Received the message
        this.messageList.push(JSON.parse(e.data));
        // console.log('channel onmessage', e.data);
    };
}
Copy the code

Create a channel on the calling side and on the receiving side. Some code is omitted.

/ / call end
socket.on('reply'.async data =>{ // Reply received
    this.loading = false;
    switch (data.type) {
        case '1': / / agree
            this.isCall = data.self;
            // Create a peer after the peer agrees
            await this.createP2P(data);
            / / DataChannel is established
            await this.createDataChannel();
            // And send an offer
            this.createOffer(data);
            break; ...}});Copy the code
/ / the receiving end
socket.on('apply'.data= > { // The request was received...this.$confirm(data.self + 'To request a video call from you, do you agree? '.'tip', {
        confirmButtonText: '同意'.cancelButtonText: 'refuse'.type: 'warning'
    }).then(async() = > {await this.createP2P(data); // Create a peer and wait for the offer
        await this.onDataChannel(); / / receive DataChannel...}). The catch (() = >{...}); });Copy the code

chat

After the connection is successful, you can have a simple chat, and the chestnut before the API is basically the same. DataChannel also supports file transfer, which we’ll talk about ata later opportunity. 💘🍦🙈Vchat – from head to toe, a social chat system (vue + node + mongodb).

send(arr) { // Send a message
    if (arr[0= = ='text') {
        let params = {account: this.account, time: this.formatTime(new Date()), mes: this.sendText, type: 'text'};
        this.channel.send(JSON.stringify(params));
        this.messageList.push(params);
        this.sendText = ' ';
    } else { // Handle data synchronization
        this.channel.send(JSON.stringify(arr)); }}Copy the code

Sketchpad synchronization

We’ve been saying that we need to sync our artboards to each other, but when do we trigger the synchronization? What data do you need to synchronize? As we mentioned earlier when encapsulating the Artboard class, all data needed for drawing is passed as parameters.

this.line(this.last, now, this.lineWidth, this.drawColor);
Copy the code

So it’s easy to imagine that we just need to send each other the data we need to draw and the type of action (maybe retracting, moving forward, etc.) every time we draw. Here we use a callback function to tell the page when to start sending data to each other.

/ / there are omitted
constructor(canvas, {moveCallback}){...this.moveCallback = moveCallback || function () {}; // Mouse movement callback
}
onmousemove(e) { // Mouse movement
    this.isMoveCanvas = true;
    let endx = e.offsetX;
    let endy = e.offsetY;
    let width = endx - this.x;
    let height = endy - this.y;
    let now = [endx, endy]; // The current position moved to
    switch (this.drawType) {
        case 'line' : {
            let params = [this.last, now, this.lineWidth, this.drawColor];
            this.moveCallback('line'. params);this.line(... params); }break;
        case 'rect' : {
            let params = [this.x, this.y, width, height, this.lineWidth, this.drawColor];
            this.moveCallback('rect'. params);this.rect(... params); }break;
        case 'polygon' : {
            let params = [this.x, this.y, this.sides, width, height, this.lineWidth, this.drawColor];
            this.moveCallback('polygon'. params);this.polygon(... params); }break;
        case 'arc' : {
            let params = [this.x, this.y, width, height, this.lineWidth, this.drawColor];
            this.moveCallback('arc'. params);this.arc(... params); }break;
        case 'eraser' : {
            let params = [endx, endy, this.width, this.height, this.lineWidth];
            this.moveCallback('eraser'. params);this.eraser(... params); }break; }}Copy the code

It looks ugly, but there’s a reason for that. First, moveCallback cannot be placed under the corresponding operation function, because all operations are synchronous. Some values will change after drawing, such as last and now. After drawing, they will be equal.

Second, you cannot write moveCallback inside the corresponding operation function, otherwise it will loop indefinitely. If you draw a line, Callback tells the caller to draw the same line, and the caller calls the line method to draw the same line. Callback is inside the line method, and it immediately has to tell you back and forth, so that you get to know each other again. It’s gonna cause some trouble anyway.

After receiving the Callback notification, the page directly calls the send method to send the data to the other side.

moveCallback(. arr) { // Synchronize to each other
    this.send(arr);
},
send(arr) { // Send a message
    if (arr[0= = ='text'{...})else { // Handle data synchronization
        this.channel.send(JSON.stringify(arr)); }}Copy the code

After receiving the data, the corresponding method of the encapsulation class is called to draw.

handleChannel(channel) { / / processing channel· · · channel. The onmessage =(e) = > { Common message types are objects
        if (Array.isArray(JSON.parse(e.data))) { // If an array is received, structure it
            let [type, ...arr] = JSON.parse(e.data);
            this.palette[type](... arr);// Call the corresponding method
        } else {
            this.messageList.push(JSON.parse(e.data)); // Receive normal messages
        }
        // console.log('channel onmessage', e.data);
    };
}
Copy the code

conclusion

This concludes the main content of this issue, which covers the use of RTCDataChannel, a simple whiteboard demonstration, and shared sketchboards for two people to collaborate on. Because a lot of content is based on the last issue of the example transformation, so omit some basic code, students who are not easy to understand suggested that the two issues combined to see (I am quite wordy, said several times back and forth, mainly hope that you can have a harvest when watching).

Communication group

Qq front-end communication group: 960807765, welcome all kinds of technical exchanges, looking forward to your joining

Afterword.

If you see here, and this article is a little help to you, I hope you can move a small hand to support the author, thank 🍻. If there is something wrong in the article, we are welcome to point out and encourage each other.

  • This article’s sample source library webrtC-Stream
  • The article warehouse 🍹 🍰 fe – code

More articles:

  • Vue component communication mode complete version
  • JavaScript prototype and prototype chain and Canvas captcha practice
  • [2019 front-end advancement] Stop, you this Promise!
  • 【 From head to toe 】 a multiplayer video chat – front-end WebRTC combat (a)
  • A social chat system (vue + Node + mongodb) – 💘🍦🙈Vchat

Agora SDK experience essay contest essay | the nuggets technology, the campaign is underway