Agenda
- Brief introduction to technology Stack
- WebRTC knowledge
- GraphQL knowledge
- The SDK framework
- Code implementation
- conclusion
Code Base First
Github Repo Portal 🚪
Brief introduction to technology Stack
Apollo GraphQL
What is a GraphQL?
In September 2015, Facebook launched GraphQL, a declarative, structured query language. The GraphQL Spec was designed to provide an alternative to RESTful architecture. In July 2018, the GraphQL project was transferred to the newly created GraphQL Foundation.
GraphQL is essentially a query language that does not require network layer selection (usually HTTP). There is also no requirement to transfer the data format (usually JSON). There is not even a requirement for the application architecture (typically a front-end separation architecture). It’s just a query language, on the same level as RESTful and RPC.
GraphQL uses a Schema to query data, and the schema describes the data structure that the client wants, regardless of the implementation of the back-end (of course, the back-end also needs to complete the logic of the corresponding data structure).
Apollo GraphQL
In actual GraphQL-based development, there are a number of excellent GraphQL frameworks out there to help us focus on requirements. Commonly used libraries are: Relay(React son), and the most complete Apollo.
Apollo is a platform for building a unified graph, a communication layer that helps you manage the flow of data between your application clients (such as web and native apps) and your back-end services. At the heart of the graph is a query language called GraphQL.
If you want to learn more about Apollo, you can visit tutorial~ Portal 🚪
Typescript
Type safety
We talked about GraphQL earlier, and the GraphQL specification ensures that the layers of our API requests are strongly typed. This means that the query can be validated for syntactic correctness and validity before it is executed, eliminating runtime errors. The strongly typed pattern also allows us to fully understand Shape of Thing, which is also very efficient to read in business development. That’s why GraphQL has been growing in popularity over the years.
Type safety refers to the benefits of using strongly typed languages and schemas, which can help developers write code faster and reduce errors. Type safety at the API level alone does not reap all the benefits that can be provided. If we use Typescript in development, we can eliminate as many syntactic errors as possible in development and production.
We hope you already have some familiarity with Typescript. If you don’t, check out the official tutorial or other resources in the community:
- TS Chinese website
- typescriptlang.org
- [Denver] 1.2 W word | great TypeScript introductory tutorial
Use Typescript+GraphQL for server-side development
Let’s talk about server-side development first. There are two common development patterns for GraphQL server-side development using Typescript. Code First and Schema First.
Code First
When choosing code-first development, we typically use TypeScript features such as: Decorators, interfaces, types, etc. are developed first, then converted at run time by some GrapQL Schema generation tools and fed to the GraphQL Server.
Pros
- A single data source is guaranteed because the TS type definition is saved simultaneously
GraphQL Schema
Define and provide a type toResolver
(the equivalent ofMVC
In the architectureController
) use; - Code-first can easily overcome the difficulties of pattern-first approaches without the need for extensive tools;
- If business complexity is going to increase, it will be easier for us to manage projects using code first.
Cons
- Simultaneously save simultaneously
GraphQL Schema
Define and provide a type toResolver
May make code less readable; - API design is more influenced by code implementation than business logic;
- The code is likely to be backward incompatible.
Schema First
Pros
- In most cases, using this pattern makes for good API design;
- By following the dependency inversion principle (DIP), code definitions are more abstract without complex dependencies;
- The front and back ends can reuse the same copy
Schema
To improve development efficiency (Schema
Easy to mock, such as usinggraphql-editor
Simulate back-end functionality 🚀).
Cons
Schema
Must be connected toResolver
Keep in sync, otherwise it could cause serious problems;- Resulting in code redundancy because SDL definitions are not easily reusable;
- multiple
Schema
Assemble into a singleSchema
It will be more difficult (butApollo3
That has been fixed!) .
Client development
See Apollo’s official documentation at portal 🚪
WebRTC
WebRTC is an open source project that enables real-time communication of audio, video, and data in Web applications. In real – time communication, the acquisition and processing of audio and video is a very complicated process. Such as audio and video streaming codec, noise reduction and echo cancellation, but in WebRTC, all this is done by the browser’s low-level encapsulation. We can take optimized media streams directly and output them to local screens and speakers, or forward them to their peers. To make a long story short, it is an API that enables web browsers to conduct real-time voice, video, and data transmission.
WebRTC was outlined at the Google I/O conference in 2013: io13webrtc.appspot.com/#1
WebRTC has achieved the development of standards for real-time communication, plug-in free audio and video data transmission, the requirements are:
- Many web services already use RTC, but require downloads, native applications, or plug-ins;
- Downloading and installing an upgrade plug-in is complex, error-prone, and annoying;
- Plug-ins can be difficult to deploy, debug, troubleshoot, and so on;
- Plug-ins may require technology licensing, complex integration, and expensive technology;
Therefore, the guiding principle of the WebRTC project is that APIs should be open source, free, standardized, browser-built, and more efficient than existing technologies.
Description of color identification of architecture drawing:
- The purple is the Web developer API layer;
- The solid blue line is the API layer for browser vendors;
- The dotted blue line allows browser vendors to customize the implementation.
Those interested in WebRTC can get started with another part of my tutorial: Portal 🚪
RxJS
RxJS is one of the hottest libraries in Web development today. It provides powerful functional ways to handle events and centralizes integration points into an increasing number of frameworks, libraries, and utilities, making learning Rx more attractive than ever before. And it has the ability to take advantage of what you already know about languages, because it covers almost all languages. Everything reactive programming offers can seem easy if you’ve mastered it.
In a multi-person conversation scenario like the one we practiced, applications tend to be driven by many asynchronous events. RxJS can then be a useful tool for handling asynchronous events efficiently.
But…
Learning RxJS and reactive programming is hard. It has lots of concepts, lots of surface apis, and a shift in thinking from imperative to declarative style. This document will use some of the RxJS API development, but also hope to help you learn to understand RxJS. If you want to get a taste of RxJS in advance, check out this website: Portal 🚪.
WebRTC knowledge
Before we get started, we need to understand how WebRTC makes calls. Think about the pain points of the next WebRTC call. For example, how to make a real-time audio and video call on two devices with completely different network environments and multimedia hardware?
Media negotiation
Both ends of a call must negotiate the media format supported by each other.As shown in the figure above, assume that there are two devices, Peer A and Peer B. Through negotiation, the two devices know that the video codec compatible with each other is H.264. Therefore, to complete the exchange of media information, it is necessary to use the aboveSDP
The agreement.
The Session Description Protocol (SDP) describes the initialization parameters of streaming media. This protocol is published by THE IETF as RFC 2327. SDP was originally a part of the Session Announcement Protocol, or SAP. The first version was released in April 1998, but has since been widely used to work with RTSP and SIP, as well as to describe multicast sessions on its own.
Therefore, in WebRTC, media capabilities are ultimately presented through SDP. Before transmitting media data, we should first negotiate media capabilities to see which encoding methods and resolutions are supported by both sides. The method of negotiation is to exchange media capability information through the signaling server.
The variety of WebRTC media negotiation is shown in the figure above.
- The first step, Amy calls
createOffer
Method to create the Offer message. The content in the offer message is Amy’sSDP
Information. - The second step, Amy calls
setLocalDescription
Method to the local endSDP
The information is saved. - Third, Amy sends the Offer message to Bob via the signaling server.
- Fourth, after Bob receives the Offer message, he calls the setRemoteDescription method to store it.
- Fifth, Bob calls
createAnswer
Method to create an answer message, again, the contents of the answer message are Bob’sSDP
Information. - In step 6, Bob calls
setLocalDescription
Method to the local endSDP
The information is saved. - In step 7, Bob passes the Anwser message to Amy via the signaling server.
- In step 8, Amy calls after receiving the answer message
setRemoteDescription
Method to save it.
Network consultation
You need to understand each other’s network so that you can find a link to communicate with each other. First of all, the ideal steps of network negotiation are as follows:
- Obtain the extranet IP address mapping of the current end
- Network information is exchanged through signaling services
NAT
& NAT
through
NAT is network address translation, which replaces the address information in the IP packet header. NAT is usually deployed at the egress of an organization’s network and translates an internal network IP address into an egress IP address to provide public network accessibility and upper-layer protocol connectivity.
RFC1918 provides for three reserved address paragraphs:
- 10.0.0.0 – those
- Along – 172.31.255.255
- 192.168.0.0-192.168.255.255
These three ranges are in the address segment of class A,B, and C respectively. They are not assigned to specific users and are reserved by IANA as private addresses. These addresses can be used within any organization or enterprise. The difference between these addresses and other Internet addresses is that they can only be used internally and cannot be used as global routing addresses. For a network that requires Internet access but uses private IP addresses internally, deploy a NAT gateway at the egress of the organization. When packets leave the private network and enter the Internet, the source IP address is replaced with a public IP address, usually the interface address of the egress device. When an external access request reaches the destination, it appears to be initiated by the organization’s egress device, so the requested server can send the response back to the egress gateway over the Internet. The egress gateway replaces the destination address with the source host address of the private network and sends the destination address to the Intranet. In this case, the request and response from the private network host to the public network server are completed without being perceived by both ends. According to this model, a large number of Intranet hosts no longer need public IP addresses.
All NAts fall into several categories:
- static
NAT
: maps a private IP address to a public address, that is, translates a private IP address into a public IP address. - dynamic
NAT
: In this typeNAT
In, multiple private IP addresses are mapped to a public IP address pool. This is used when we know how many fixed users want to access the Internet at a given point in time. - PAT,
NAT
Overload) : useNAT
Overloading can translate many local (private) IP addresses into a single public IP address. Port numbers are used to distinguish traffic, that is, which traffic belongs to which IP address. This is the most common approach because it is cost-effective, since thousands of users can be connected to the Internet using only one real global (public) IP address.
STUN
agreement
Simple Traversal of UDP over NATs Protocol STUN is a network protocol that allows clients to find their public address after NAT. Find out which type of NAT you are behind and which Internet port the NAT binds to a local port. This information is used to create UDP communication between two hosts that are behind the NAT router. The default port number is 3478. It classifies NAT implementations into four categories:
- Full-cone
NAT
/ Full coneNAT
. All requests sent from the same Intranet IP address and port number are mapped to the same Internet IP address and port number, and any Internet host can send packets to the Intranet host using the mapped Internet IP address and port number.
- (Address)-restricted-cone
NAT
/ limit coneNAT
. It also maps all requests from the same internal IP and port number to the same external IP and port number. Unlike a full cone, an extranet host can only send packets to an Intranet host that has previously sent packets to it.
- Port-restricted cone
NAT
/ Port limit coneNAT
. And limit coneNAT
Very similar, except that it includes the port number. That is, if an extranet host with IP address X and port P wants to send packets to an Intranet host, the Intranet host must have sent packets to this IP address X and port P before.
- Symmetric
NAT
/ symmetricNAT
. All requests sent from the same Intranet IP address and port number to a specific destination IP address and port number are mapped to the same IP address and port number. If the same host uses the same source address and port number to send packets to different destinations,NAT
A different mapping will be used. In addition, only the extranet host that receives data can send packets to the Intranet host.
plan
Once the client is aware of the UDP port on the Internet side, communication can begin. If the NAT is fully conical, then either party can initiate communication. If the NAT is a restricted cone or port restricted cone, both sides must start the transmission together.
Note that it is not necessary to use STUN to use the technology described in STUN RFC; you can design a separate protocol and integrate the same functionality into the server running the protocol (TURN).
Protocols like SIP use UDP packets to transfer audio/video data over the Internet. Unfortunately, because the two ends of the communication tend to be behind the NAT, it is impossible to create a connection using traditional methods. This is where STUN comes in.
STUN is a CS protocol. A VoIP phone or software package might include a STUN client, and the RTCPeerConnection interface in WebRTC gives us the ability to call the STUN server directly. The client sends a request to the STUN server, which then reports to the STUN client the PUBLIC IP address of the NAT router and the port that the NAT has opened to allow incoming traffic back to the Intranet to assemble the correct UDP packets.
The above response also enables the STUN client to determine the type of NAT being used — because different NAT types handle incoming UDP packets differently. Three of the four main types are available: full conic NAT, restricted conic NAT, and port restricted conic NAT — but symmetric NAT (also known as bidirectional NAT), which is often used in large corporate networks, is not.
TURN
agreement
Symmetric NAT can be penetrated using the TURN protocol. The TURN protocol allows a host to use the trunk service to transmit packets with its peer. TURN differs from other relay protocols in that it allows clients to communicate simultaneously with multiple peers using a single relay address. This perfectly compensates for the STUN’s inability to penetrate symmetric NAT.
RTCPeerConnection attempts to establish direct communication between peers over UDP. If this fails, RTCPeerConnection will use TCP for the connection. If TCP still fails, the TURN server can be used as a backup to forward data between terminals. To reiterate: TURN is used to relay audio/video/data streams between peers. The TURN server has a public address, so peers can communicate with others even if they are behind a firewall or proxy. TURN servers have a conceptually simple task-relaying data streams-but unlike STUN servers, they consume a lot of bandwidth. In other words, TURN servers need to be more powerful.
The specific principle is not introduced too much, the focus of this practice is not here. Importantly, through these two protocols, we can easily obtain the current external IP address mapping to complete network negotiation.
From the above introduction, it is not difficult to conclude that if we want to establish a WebRTC call, we need to complete the front-end logic to create and exchange signaling information, and create a signaling server to forward the SDP on each end. What about multi-party calls? It’s just going to be multiple times.
GraphQL knowledge
Why GraphQL?
- In REST apis, accessing data often requires access to different interfaces, such as 🌰, assuming:
- The /users/ interface is used to fetch initial user data
- /users//posts interface returns all posts of the user
- /users// Followers Returns the list of followers of the user
On-demand access to
REST apis are prone to over-fetching, another example is 🌰 : The Mobile terminal may not need a certain field due to limited display space, but the PC PC uses this field, which is redundant for Mobile. In this case, it may need to define an additional interface or add parameters in the current interface to complete the compatibility of both ends.
In GraphQL, the client only needs to request the data they want to get on demand.
Since the document sex
GraphQL is self-documenting, with servers pre-defining data structures and query interfaces, such as the ability to view server data in GraphiQL, including query and entity types (GraphQL is a strongly typed query language).
How to use GraphQL
The GraphQL back end usually exposes a POST interface to receive queries, which means that the GraphQL interface can be accessed directly using libraries such as Fetch, Request, and even Postman, Insomnia, and curl commands.
More advantage
There is a lot of logic that can be reused in everyday Web development where GraphQL is introduced. Such as:
- Cache data from the server
- Integration with the UI framework (
React
,Angular
As well asVue
) - in
mutation
Ensure that the local cache data is consistent with that of the server - management
websocket
forsubscriptions
(Also used in this practice) - Paging query
Query & Mutation
Please read GraphQL official document: Chinese | English version
Subscription
In addition to Query and Mutation, GraphQL supports a third type of operation: Subscription.
Subscription, like Query, is used to retrieve data. Subscription, unlike Query, allows us to create a long link that changes its results over time (most commonly via WebSocket on the server), giving the server the ability to push updates to subscribe to the results.
Statement of Subscription
As with Query and Mutation, we need to define Subscription on both the server and client sides:
The service side
We represent a long link by creating a Schema of type Subscription. The commentAdded Subscription below notifies Subscription clients when a new comment is added to a particular blog post (as specified by the postID parameter).
type Subscription { commentAdded(postID: ID!) : Comment }Copy the code
The client
The client also needs to write the corresponding request Schema to indicate that it wants to make a long link request:
const COMMENTS_SUBSCRIPTION = gql`
subscription OnCommentAdded($postID: ID!) {
commentAdded(postID: $postID) {
id
content
}
}
`;
Copy the code
With this knowledge in hand, we should be able to start thinking about how to implement this multiplayer collaboration SDK.
The SDK framework
Signaling server
This example uses GraphQL Subscription to create a WebRTC signaling server.
Object type definition
The most basic component of GraphQL Schema is the object type, which defines only one kind of object and its fields that can be retrieved from the service. For our signaling server, I designed the following fields (main fields) :
Basic fields
- Channel
- Participant
- ChannelWithParticipant
WebRTC Signaling Field (from standard HTML DOM API)
- Offer
- Answer
- Candidate
Mutation
The Mutation type is a special object type in GraphQL that is used to modify server-side data. The signaling server is mainly used to exchange signaling, so here I designed the following Mutation to give the client the ability to submit signaling.
- link
- offer
- answer
- candidate
Subscription
We need to have the ability to broadcast Mutation changes to all subscribers, so the service should have the ability to be subscribed, and HERE I designed the Subscription below.
- linked
We expect when subscription is specifiedChannel
When, whenever presentChannel
There are newlink mutation
When executed, the subscriber can find out who the new subscriber is.
- offered
SDP Offer
It should also be broadcast to subscribers of the current channel.
- answered
- candidated
summary
When our signaling service implements the basic types described above,Mutation
As well asSubscription
, it can satisfy all the logic of a simple multi-person communication application. Let’s say I haveUser A
andUser B
Two people joined the signaling serverChannel A
If you want to build it onceWebRTC
The call event sequence is as follows:
Subscription allows multiple people in the same channel to communicate successfully with each other when they join the channel.
The SDK client
Combined with the above flow chart, we can first determine the SDK client needs to have several capabilities:
- Specified requested
GraphQL
The server - send
GraphQL Mutation
(link
.offer
.answer
.candidate
) - To subscribe to
GraphQL Subscription
(linked
.offered
.answered
.candidated
) - create
WebRTC PeerConnection
And created through the Connection instanceSDP Offer
.SDP Answer
As well asIceCandidate
At the same time, multi-party communication should have the ability to send and receive messages and should support some event handles:
- support
send
Method that can be called to send messages to other ends - support
onmessage
When a message is received, we can specify a handler to handle it
We can get a data structure roughly like this:
Export default class WebrtcChannelClient<ChannelMessage> {/** * constructor * @param uri GraphQL service address * @param wsuri GraphQL */ constructor(uri: string, wsuri: string, channel: channel); /** * Client user ID */ ID: string; /** * Apollo Client */ client: ApolloClient<NormalizedCacheObject>; /** * WebRTC */ connections: {id: string; connection: RTCPeerConnection; } []; */ sendChannels: RTCDataChannel[]; ReceiveChannels: RTCDataChannel[]; /** * @param message message object, specified by ChannelMessage generic */ send(message: ChannelMessage): void; /** * Supports binding event handles such as message, candidate */ addEventListener: <K extends keyof WebrtcChannelClientEventMap<ChannelMessage>>(type: K, listener: (this: WebrtcChannelClient<ChannelMessage>, ev: WebrtcChannelClientEventMap<ChannelMessage>[K]) => any) => void; }Copy the code
Code implementation
The service side
Initialize the Code Base
cd path/to/project
npm init -y
Copy the code
Install project required dependencies
"Dependencies" : {" Apollo - server - express ":" ^ 3.3.0 ", "express" : "^ 4.17.1", "graphql" : "^15.5.3", "graphqL-subscriptions ": "^1.2.1", "subscriptions-transport-ws": "^0.9.19"}, "devDependencies": {" @ types/graphql ", "^ 14.5.0", "@ graphql codegen/cli" : "2.2.0", "@ types/eslint" : "^ 7.2.13", "@ types/express" : "^ 4.17.12 @ types/"," node ":" ^ 16.0.0 ", "@ typescript - eslint/eslint - plugin" : "^ 4.28.1", "@ typescript - eslint/parser" : "^ 4.28.1 eslint", ""," ^ 7.30.0 ", "eslint - config - reality - the typescript" : "^ 12.3.1", "eslint - plugin - import" : "^ 2.23.4 nodemon", ""," ^ 2.0.9 ", "ts - node" : "^ 10.0.0", "typescript" : "^ 4.4.2"},Copy the code
The main runtime dependencies are:
apollo-server-express
:express
middleware for Apollo Serverexpress
: the most popular server framework for Nodegraphql
: GraphQL basegraphql-subscriptions
: GraphQL Subscription Basesubscriptions-transport-ws
: used to implement Apollo Subscription Server
Beginning in Apollo Server 3, subscriptions are not supported by the “batteries-included”
apollo-server
package. To enable subscriptions, you must first swap to theapollo-server-express
package (or any other Apollo Server integration package that supports subscriptions).
As stated on the Apollo website, starting with Apollo Server 3, we needed Express to use subscription, which is why we added Express as an extra in the project.
The development dependencies are:
typescript
: Enable our project to support Typescriptts-node
: node. JsTypeScript
Code interpreter and runnernodemon
: Automatically help us restart the service when we encounter file changes during developmenteslint
: Specification code@graphql-codegen/cli
: GraphQL Schema > Typescript code generator@types/*
Typescript type assist
Create tsconfig
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es6",
"sourceMap": true,
"baseUrl": ".",
"outDir": "./build",
"incremental": true,
"lib": [
"esnext.asynciterable"
]
},
"exclude": [
"node_modules"
]
}
Copy the code
Schema First
As we mentioned earlier, in most cases, using this pattern makes it easier to design a good API, so here we use Schema First to create an application:
// schema/channel.schema.ts import { gql } from 'apollo-server-express'; Export const channelSchema = GQL '""" Subscriber, subscribe to all messages of the corresponding signaling channel """ type Participant {""" Subscriber ID"" ID: String! } """ type Channel {""" signaling Channel ID""" ID: String! } """ parameter required for creating a signaling channel """ INPUT ChannelInput {""" Signaling channel ID"" ID: String! } """ "input ParticipantInput {""" subscriber ID"" ID: String! } """ "Signaling channel object, which also contains information about the subscriber currently joining the signaling channel. """ type ChannelWithParticipant {""" Signaling Channel ID""" ID: String! """ Current subscriber """ Participant: Participant! } type Query {default: String} type Mutation {""" "link(""" """ "channel: ChannelInput! """ Subscriber parameter, """ Participant: ParticipantInput! : Channel! """ Sends SDP Offer""" Offer (""" Signaling channel parameter, which contains the signaling channel ID""" channel: ChannelInput! """ From: ParticipantInput! """ To: ParticipantInput! """SDP Offer""" offer: TransferRTCSessionDescriptionInput! ) : Boolean! """ Send SDP Answer""" Answer (""" "Signaling channel parameter, which contains the SIGNALING channel ID""" channel: ChannelInput! ""Answer from who "" from: ParticipantInput! """Answer who to send to """ to: ParticipantInput! """SDP Answer""" answer: TransferRTCSessionDescriptionInput! ) : Boolean! """ Send SDP Candidate""" Candidate (""" Signaling channel parameter, which contains the SIGNALING channel ID""" channel: ChannelInput! """Candidate from: ParticipantInput! """Candidate to whom """ to: ParticipantInput! """RTC Ice Candidate""" candidate: TransferRTCIceCandidateInput! ) : Boolean! } type Subscription {""" link mutation""" Linked (channel: ChannelInput!) : ChannelWithParticipant! """ subscribe to offer mutation""" offered(channel: ChannelInput!) : Offer! """ Subscribe to answer mutation""" Answered (Channel: ChannelInput!) for the specified signaling channel. : Answer! """ subscribe to candidate mutation""" candidated(channel: ChannelInput!) : Candidate! } `;Copy the code
// schema/signaling.schema.ts import { gql } from 'apollo-server-express'; Export const signalingSchema = GQL '""SDP Offer object """ type Offer {""" Signal channel to which Offer is sent "" channel: channel! """ "From: Participant! """ To: Participant! """RTCSessionDescription details please refer to MDN""" Offer: RTCSessionDescription} """SDP Answer object """ type Answer {"""Answer Signaling channel """ channel: channel! """Answer's sender """ from: Participant! """ To: Participant! """RTCSessionDescription details please refer to MDN""" answer: RTCSessionDescription} """IceCandidate object, which is exchanged between P2P parties, thus completing the establishment of a communication. """ type Candidate {"""Candidate sent signaling channel "" channel: Channel! """Candidate's sender """ from: Participant! """Candidate's receiver """ to: Participant! RTCIceCandidate: MDN""" candidate: RTCIceCandidate} type RTCSessionDescription {SDP: String type: RTCSdp } type RTCIceCandidate { candidate: String component: RTCIceComponent foundation: String port: Int priority: Int protocol: RTCIceProtocol relatedAddress: String relatedPort: Int sdpMLineIndex: Int sdpMid: String tcpType: RTCIceTcpCandidate type: RTCIceCandidateType usernameFragment: String } enum RTCSdp { answer offer pranswer rollback } enum RTCIceComponent { rtcp rtp } enum RTCIceProtocol { tcp udp } enum RTCIceTcpCandidate { active passive so } enum RTCIceCandidateType{ host prflx relay srflx } input TransferRTCSessionDescriptionInput { sdp: String type:RTCSdp } input TransferRTCIceCandidateInput { candidate: String component: RTCIceComponent foundation: String port: Int priority: Int protocol: RTCIceProtocol relatedAddress: String relatedPort: Int sdpMLineIndex: Int sdpMid: String tcpType: RTCIceTcpCandidate type: RTCIceCandidateType usernameFragment: String } `;Copy the code
Note: “”” “***””” is the format of GraphQL Docs. Using this annotation helps us to generate the corresponding interface document.
Create the Apollo Server
// main.ts import { createServer } from 'http'; import { execute, subscribe } from 'graphql'; import { SubscriptionServer } from 'subscriptions-transport-ws'; import { makeExecutableSchema } from '@graphql-tools/schema'; import express from 'express'; import { ApolloServer } from 'apollo-server-express'; Schema import {channelSchema} from './schema/channel.schema'; import { signalingSchema } from './schema/signaling.schema'; (async function start() {// create express app const app = express(); // create node const httpServer = createServer(app); Const schema = makeExecutableSchema({typeDefs: [ channelSchema, signalingSchema, ], }); // Create ApolloServer and destroy subscriptionServer as soon as the Server is destroyed Const Server = new ApolloServer({schema, plugins: [{ async serverWillStart() { return { async drainServer() { // eslint-disable-next-line @typescript-eslint/no-use-before-define subscriptionServer.close(); }, }; }, }], }); / / create subscriptionServer const subscriptionServer = subscriptionServer. Create ({schema, execute, subscribe}, {server: httpServer, path: server.graphqlPath, }, ); // Start Apollo Server and add Express Middleware await server.start(); server.applyMiddleware({ app }); const PORT = 4000; // Start HTTP server httpserver. listen(PORT, () => console.log(`Server is now running on http://localhost:${PORT}/graphql`)); } ());Copy the code
Start the service
We can start our service with ts-Node and Nodemon:
nodemon --exec 'ts-node ./src/main.ts'
Copy the code
Edit package.json and add the command to ‘scripts’ :
"scripts": {
"start": "nodemon --exec 'ts-node ./src/main.ts'"
}
Copy the code
Generate the typescript types
In the service starts, we can visit http://localhost:4000/graphql to retrieve we just created Schema; Use @graphql-codeGen /cli to convert the Schema into Typescript files.
NPX graphql -codeGen init # What type of application are you building? ❯◉ backend-API or server infection was borne out of Angular infection Application built infection with React infection Application built with Stencil infection was borne out into infection built with other framework or Vanilla JS? Where is your schema? : (path or url) https://localhost:4000/graphql ? Pick plugins: ◉ TypeScript (required by other TypeScript plugins) ◉ TypeScript Resolvers (strongly typed resolve functions) infection TypeScript MongoDB (typed MongoDB objects) ❯◉ TypeScript GraphQL Document Nodes (Embedded GraphQL Document)? Where to write the output: src/type.ts ? Do you want to generate an introspection file? (Y/n) Y ? How to name the config file? codegen.yml ? What script in package.json should run the codegen? codegenCopy the code
Create GraphQL Resolver
We can create an enum to hold the Subscription.
// constant.ts
export enum TOPIC {
linked = 'linked',
offered = 'offered',
answered = 'answered',
candidated = 'candidated',
}
Copy the code
Then create the GraphQL Resolver.
// channel.resolver.ts import { ApolloError } from 'apollo-server-express'; import { withFilter, PubSub } from 'graphql-subscriptions'; Import {Answer, Candidate, Channel, ChannelWithParticipant, MutationAnswerArgs, MutationCandidateArgs, MutationLinkArgs, MutationOfferArgs, Offer, Participant, Resolvers, SubscriptionLinkedArgs, SubscriptionOfferedArgs, } from '.. /type'; import { TOPIC } from '.. /constant'; const pubsub = new PubSub(); export const channelResolver: Resolvers = { Subscription: { linked: {// Here withFilter is used to restrict sending a message to subscribers who subscribe to the specified Channel: withFilter( () => pubsub.asyncIterator([TOPIC.linked]), ( payload: { linked: Channel }, variables: SubscriptionLinkedArgs, ) => payload.linked.id === variables.channel.id, ), }, offered: { subscribe: withFilter( () => pubsub.asyncIterator([TOPIC.offered]), ( payload: { offered: Offer }, variables: SubscriptionOfferedArgs, ) => payload.offered.channel.id === variables.channel.id, ), }, answered: { subscribe: withFilter( () => pubsub.asyncIterator([TOPIC.answered]), ( payload: { answered: Answer }, variables: SubscriptionOfferedArgs, ) => payload.answered.channel.id === variables.channel.id, ), }, candidated: { subscribe: withFilter( () => pubsub.asyncIterator([TOPIC.candidated]), ( payload: { candidated: Candidate }, variables: SubscriptionOfferedArgs, ) => payload.candidated.channel.id === variables.channel.id, ), }, }, Mutation: { async link(_, args: MutationLinkArgs) { try { const channel = { id: args.channel.id, participant: args.participant as Participant, } as ChannelWithParticipant; Publish (top. linked, {[top. linked]: channel,} as {linked: ChannelWithParticipant }); return args.channel; } catch (e) { throw new ApolloError(e.message); }}, async offer(_, args: MutationOfferArgs) {// Broadcast offer to subscriber await pubsub.publish(topic. offered, {offered: args, } as { offered: Offer }); return true; }, async answer(_, args: MutationAnswerArgs) { await pubsub.publish(TOPIC.answered, { answered: args, } as { answered: Answer }); return true; }, async candidate(_, args: MutationCandidateArgs) { await pubsub.publish(TOPIC.candidated, { candidated: args, } as { candidated: Candidate }); return true; ,}}};Copy the code
Finally, import the newly created resolver file from main.ts:
import { channelResolver } from './resolver/channel.resolver';
const schema = makeExecutableSchema({
typeDefs: [
channelSchema,
signalingSchema,
],
resolvers: [
channelResolver,
],
});
Copy the code
Run the service and view the results
npm start
Copy the code
The client
Initialize the Code Base
cd path/to/project
npm init -y
Copy the code
Install project required dependencies
"Dependencies" : {" @ Apollo/client ":" ^ 3.4.13 ", "graphql" : "^ 15.6.0", "RXJS" : "^ 7.3.0", "subscriptions - transport - ws" : "^ 0.9.19 uuid", "" :" ^ 8.3.2 "}, "devDependencies" : {" @ graphql codegen/cli ":" 2.2.0 ", "@ types/uuid" : "^ 8.3.1", "casual" : "^ 1.6.2", "the fork - ts - the checker - webpack - plugin" : "^ per paragraph 6.3.3," ts - loader ":" ^ 9.2.5 ", "webpack" : "^ 5.52.0", "webpack - cli" : "^ 4.8.0" "@ types/graphql" : "^ 14.5.0", "@ types/eslint" : "^ 7.2.13", "@ types/node" : "^ 16.0.0", "@ typescript - eslint/eslint - plugin" : "^ 4.28.1", "@ typescript - eslint/parser" : "^ 4.28.1", "eslint" : "^ 7.30.0 eslint - config -", "reality - the typescript" : "^ 12.3.1", "eslint - plugin - import" : "^ 2.23.4", "ts - node" : "^ 10.0.0," "typescript" : "^ 4.4.2"}, "peerDependencies" : {" react ":" ^ 17.0.2 "},Copy the code
The main runtime dependencies are:
@apollo/client
: Apollo GraphQL client- This dependency is also introduced
react
In order not to be included in our SDK build productreact
Lead to more than onereact
The package failed to start the application, declared here aspeerDependencies
To do this, we also need to adjust the way our code is built.
- This dependency is also introduced
rxjs
: Handles asynchronous event flowssubscriptions-transport-ws
: Webscoket Subscription Clientuuid
: Generates a random ID
The development dependency mainly adds Webpack to build SDK code.
GraphQL Codegen
GraphQL Codegen converts GraphQL Schema to Typescript on the server. On the client side, we need to repeat the above steps.
npx graphql-codegen init
npm run codegen
Copy the code
Create the Apollo Client
// subscription-client.ts
import {
ApolloClient, HttpLink, InMemoryCache, split,
} from '@apollo/client';
import { WebSocketLink } from '@apollo/client/link/ws';
import { getMainDefinition } from '@apollo/client/utilities';
export const createSubscriptionClient = (uri: string, wsuri: string) => {
const httpLink = new HttpLink({
uri,
});
const wsLink = new WebSocketLink({
uri: wsuri,
options: {
reconnect: true,
},
});
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition'
&& definition.operation === 'subscription'
);
},
wsLink,
httpLink,
);
return new ApolloClient({
uri,
cache: new InMemoryCache(),
link: splitLink,
});
};
Copy the code
Create WebRtcChannelClient
We can quickly create Mutation and Subscription using the type we just generated and the GraphQL Client instance:
private mutationLink(args: MutationLinkArgs) {
return this.client.mutate<
{ link: Channel },
MutationLinkArgs
>({
mutation: MUTATION_LINK,
variables: args,
});
}
private mutationOffer(args: MutationOfferArgs) {
return this.client.mutate<
{ offer: boolean },
MutationOfferArgs
>({
mutation: MUTATION_OFFER,
variables: args,
});
}
private mutationAnswer(args: MutationAnswerArgs) {
return this.client.mutate<
{ answer: boolean },
MutationAnswerArgs
>({
mutation: MUTATION_ANSWER,
variables: args,
});
}
private mutationCandidate(args: MutationCandidateArgs) {
return this.client.mutate<
{ candidate: boolean },
MutationCandidateArgs
>({
mutation: MUTATION_CANDIDATE,
variables: args,
});
}
private subscriptionLinked(args: SubscriptionLinkedArgs) {
return this.client.subscribe<
{ linked: ChannelWithParticipant },
SubscriptionLinkedArgs
>({
query: SUBSCRIPTION_LINKED,
variables: args,
});
}
private subscriptionOffered(args: SubscriptionOfferedArgs) {
return this.client.subscribe<
{ offered: Offer },
SubscriptionOfferedArgs
>({
query: SUBSCRIPTION_OFFERED,
variables: args,
});
}
private subscriptionAnswered(args: SubscriptionAnsweredArgs) {
return this.client.subscribe<
{ answered: Answer },
SubscriptionAnsweredArgs
>({
query: SUBSCRIPTION_ANSWERED,
variables: args,
});
}
private subscriptionCandidate(args: SubscriptionCandidatedArgs) {
return this.client.subscribe<
{ candidated: Candidate },
SubscriptionCandidatedArgs
>({
query: SUBSCRIPTION_CANDIDATED,
variables: args,
});
}
Copy the code
We want the SDK to do a link mutation on the server as soon as it is created:
private triggerConnection(channel: Channel) {Mutation of(null).pipe(delay(500)).subscribe(async () => {await this.mutationlink ({{await this. channel: { id: channel.id }, participant: { id: this.id, }, }); }); }Copy the code
At the same time, I summarized all the events that could be triggered, and summarized as RxJS Subject:
private connection$ = new Subject<{ id: string, connection: RTCPeerConnection }>();
private sendChannel$ = new Subject<RTCDataChannel>();
private receiveChannel$ = new Subject<RTCDataChannel>();
private linked$ = new Subject<ChannelWithParticipant>();
private offered$ = new Subject<Offer>();
private answered$ = new Subject<Answer>();
private candidated$ = new Subject<Candidate>();
private message$ = new Subject<ChannelMessage>();
private connected$ = new Subject<void>();
Copy the code
When these subjects receive new values, the corresponding methods are fired:
private monitorConnectionSubjects(channel: Channel) { this.sendChannel$.subscribe((sendChannel) => this.sendChannels.push(sendChannel)); this.receiveChannel$.subscribe((receiveChannel) => { this.receiveChannels.push(receiveChannel); this.enableReceivedChannels(); }); this.connection$.subscribe((connection) => this.connections.push(connection)); this.linked$.subscribe(async (channelWithParticipant) => { const { id, participant } = channelWithParticipant; if (this.id === participant.id) return; const peerConnection = await this.createRTCPeerConnectionAndSetupSendChannel( PeerConnectionType.OFFER, { channel: { id }, participant } as MutationLinkArgs, ); const offer = await peerConnection.createOffer(); await peerConnection.setLocalDescription(offer); await this.mutationOffer({ channel: { id: channel.id }, from: { id: this.id }, to: { id: participant.id }, offer: offer as TransferRtcSessionDescriptionInput, }); }); this.offered$.subscribe(async (offer) => { if (this.id ! == offer.to.id) return; if (this.id === offer.from.id) return; const peerConnection = await this.createRTCPeerConnectionAndSetupSendChannel( PeerConnectionType.ANSWER, offer as MutationOfferArgs, ); await peerConnection.setRemoteDescription(offer.offer as RTCSessionDescriptionInit); const answer = await peerConnection.createAnswer(); await peerConnection.setLocalDescription(answer); await this.mutationAnswer({ channel: { id: channel.id }, from: { id: this.id }, to: { id: offer.from.id }, answer: answer as TransferRtcSessionDescriptionInput, }); }); this.answered$.subscribe(async (answer) => { if (this.id ! == answer.to.id) return; if (this.id === answer.from.id) return; const peerConnection = this.connections.find((c) => c.id === answer.from.id); if (! peerConnection) return; await peerConnection.connection .setRemoteDescription(answer.answer as RTCSessionDescriptionInit); }); this.candidated$.subscribe(async (candidate) => { if (this.id ! == candidate.to.id) return; if (this.id === candidate.from.id) return; const peerConnection = this.connections.find((c) => c.id === candidate.from.id); if (! peerConnection) return; await peerConnection.connection.addIceCandidate(candidate.candidate as RTCIceCandidate); }); }Copy the code
For creating RTCPeerConnection, the logic for sender and receiver is roughly the same except for some slight differences, so we can declare a method to create RTCPeerConnection as well as DataChannels:
// Specify the current PeerConnection type. Create enum PeerConnectionType {OFFER, Answer,}Copy the code
private async createRTCPeerConnectionAndSetupSendChannel( type: PeerConnectionType, args: MutationLinkArgs | MutationOfferArgs, ) { const peerConnection = new RTCPeerConnection(); let sendChannel: RTCDataChannel; if (type === PeerConnectionType.OFFER) { const { participant } = args as MutationLinkArgs; sendChannel = peerConnection.createDataChannel(participant.id); this.connection$.next({ id: participant.id, connection: peerConnection }); } else if (type === PeerConnectionType.ANSWER) { const { from } = args as MutationOfferArgs; sendChannel = peerConnection.createDataChannel(from.id); this.connection$.next({ id: from.id, connection: peerConnection }); } peerConnection.onicecandidate = async (e) => { const { candidate } = e; if (! candidate) return; if (type === PeerConnectionType.OFFER) { const { participant, channel } = args as MutationLinkArgs; await this.mutationCandidate({ channel: { id: channel.id }, from: { id: this.id, }, to: { id: participant.id }, candidate: candidate as TransferRtcIceCandidateInput, }); return; } if (type === PeerConnectionType.ANSWER) { const { channel, from, to } = args as MutationOfferArgs; await this.mutationCandidate({ channel: { id: channel.id }, from: { id: to.id }, to: { id: from.id }, candidate: candidate as TransferRtcIceCandidateInput, }); }}; peerConnection.ondatachannel = (e) => { const { channel } = e; if (! channel) return; this.receiveChannel$.next(channel); }; this.sendChannel$.next(sendChannel); return peerConnection; }Copy the code
Another advantage of using RxJS is that we can easily expose events to SDK consumers at the same time. Here we can declare an addEventListener method that mimics the DOM API:
public addEventListener: <K extends keyof WebrtcChannelClientEventMap<ChannelMessage>>( type: K, listener: ( this: WebrtcChannelClient<ChannelMessage>, ev: WebrtcChannelClientEventMap<ChannelMessage>[K]) => any ) => void = (type, listener) => { switch (type) { case 'message': this.message$.subscribe((message) => { listener.call(this, message); }); break; case 'candidate': this.receiveChannel$ .pipe(delay(1_000)) .subscribe(() => { listener.call(this); }); break; case 'connected': this.connected$.subscribe(() => { listener.call(this); }); break; default: break; }};Copy the code
You may have noticed the ChannelMessage generic in the code above. WebRTC DataChannel transfers only support the content of Stringify, but with generic constraints we can easily ensure that Stringify’s content can be parsed back to its original type. And with generic constraints, we get plenty of code hints when we write code, so we don’t have to worry about what data WebRTC DataChannel actually transmits:
export default class WebrtcChannelClient<ChannelMessage> {}
Copy the code
public send(message: ChannelMessage) { try { this.sendChannels.forEach((channel) => { if (typeof message === 'string') { channel.send(message); } else { channel.send(JSON.stringify(message)); }}); } catch (e) { console.log(e); }}Copy the code
private message$ = new Subject<ChannelMessage>();
Copy the code
WebRTC Datachannel After the parse the trigger $message Subject private enableReceivedChannels () {this. ReceiveChannels. ForEach ((channel) = > {/ / eslint-disable-next-line no-param-reassign channel.onmessage = (ev) => { const { data } = ev; if (! data) return; try { this.message$.next(JSON.parse(data) as ChannelMessage); } catch { this.message$.next(data as ChannelMessage); }}; }); }Copy the code
Exclusive version of the client code is as follows:
import { ApolloClient, NormalizedCacheObject } from '@apollo/client';
import { v4 as uuid } from 'uuid';
import {
delay, of, Subject,
} from 'rxjs';
import { createSubscriptionClient } from './subscription-client';
import {
Channel, ChannelWithParticipant,
Offer, Answer, Candidate,
MutationLinkArgs, MutationOfferArgs,
MutationAnswerArgs, MutationCandidateArgs,
SubscriptionLinkedArgs, SubscriptionOfferedArgs,
SubscriptionAnsweredArgs, SubscriptionCandidatedArgs,
TransferRtcIceCandidateInput,
TransferRtcSessionDescriptionInput,
} from './type';
import {
MUTATION_LINK, MUTATION_OFFER,
MUTATION_ANSWER, MUTATION_CANDIDATE,
SUBSCRIPTION_LINKED, SUBSCRIPTION_OFFERED,
SUBSCRIPTION_ANSWERED, SUBSCRIPTION_CANDIDATED,
} from './api';
enum PeerConnectionType {
OFFER,
ANSWER,
}
interface WebrtcChannelClientEventMap<ChannelMessage> {
message: ChannelMessage,
candidate: void
connected: void
}
export default class WebrtcChannelClient<ChannelMessage> {
constructor(uri: string, wsuri: string, channel: Channel) {
this.client = createSubscriptionClient(uri, wsuri);
this
.subscriptionLinked({ channel })
.subscribe(({ data }) => {
if (data?.linked) this.linked$.next(data.linked);
});
this
.subscriptionOffered({ channel })
.subscribe(({ data }) => {
if (data?.offered) this.offered$.next(data.offered);
});
this
.subscriptionAnswered({ channel })
.subscribe(({ data }) => {
if (data?.answered) this.answered$.next(data.answered);
});
this
.subscriptionCandidate({ channel })
.subscribe(({ data }) => {
if (data?.candidated) this.candidated$.next(data.candidated);
});
this.monitorConnectionSubjects(channel);
this.triggerConnection(channel);
}
public id: string = uuid() + Date.now();
public client: ApolloClient<NormalizedCacheObject>;
public connections: { id: string, connection: RTCPeerConnection }[] = [];
public sendChannels: RTCDataChannel[] = [];
public receiveChannels: RTCDataChannel[] = [];
public addEventListener: <K extends keyof WebrtcChannelClientEventMap<ChannelMessage>>(
type: K,
listener: (
this: WebrtcChannelClient<ChannelMessage>,
ev: WebrtcChannelClientEventMap<ChannelMessage>[K]) => any
) => void = (type, listener) => {
switch (type) {
case 'message':
this.message$.subscribe((message) => {
listener.call(this, message);
});
break;
case 'candidate':
this.receiveChannel$
.pipe(delay(1_000))
.subscribe(() => {
listener.call(this);
});
break;
case 'connected':
this.connected$.subscribe(() => {
listener.call(this);
});
break;
default:
break;
}
};
public connected() {
this.connection$.complete();
this.sendChannel$.complete();
this.receiveChannel$.complete();
this.linked$.complete();
this.offered$.complete();
this.candidated$.complete();
this.connected$.next();
this.connected$.complete();
this.enableReceivedChannels();
}
public finish() {
this.message$.complete();
}
public send(message: ChannelMessage) {
try {
this.sendChannels.forEach((channel) => {
if (typeof message === 'string') {
channel.send(message);
} else {
channel.send(JSON.stringify(message));
}
});
} catch (e) {
console.log(e);
}
}
private connection$ = new Subject<{ id: string, connection: RTCPeerConnection }>();
private sendChannel$ = new Subject<RTCDataChannel>();
private receiveChannel$ = new Subject<RTCDataChannel>();
private linked$ = new Subject<ChannelWithParticipant>();
private offered$ = new Subject<Offer>();
private answered$ = new Subject<Answer>();
private candidated$ = new Subject<Candidate>();
private message$ = new Subject<ChannelMessage>();
private connected$ = new Subject<void>();
private monitorConnectionSubjects(channel: Channel) {
this.sendChannel$.subscribe((sendChannel) => this.sendChannels.push(sendChannel));
this.receiveChannel$.subscribe((receiveChannel) => {
this.receiveChannels.push(receiveChannel);
this.enableReceivedChannels();
});
this.connection$.subscribe((connection) => this.connections.push(connection));
this.linked$.subscribe(async (channelWithParticipant) => {
const { id, participant } = channelWithParticipant;
if (this.id === participant.id) return;
const peerConnection = await this.createRTCPeerConnectionAndSetupSendChannel(
PeerConnectionType.OFFER,
{ channel: { id }, participant } as MutationLinkArgs,
);
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
await this.mutationOffer({
channel: { id: channel.id },
from: { id: this.id },
to: { id: participant.id },
offer: offer as TransferRtcSessionDescriptionInput,
});
});
this.offered$.subscribe(async (offer) => {
if (this.id !== offer.to.id) return;
if (this.id === offer.from.id) return;
const peerConnection = await this.createRTCPeerConnectionAndSetupSendChannel(
PeerConnectionType.ANSWER,
offer as MutationOfferArgs,
);
await peerConnection.setRemoteDescription(offer.offer as RTCSessionDescriptionInit);
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
await this.mutationAnswer({
channel: { id: channel.id },
from: { id: this.id },
to: { id: offer.from.id },
answer: answer as TransferRtcSessionDescriptionInput,
});
});
this.answered$.subscribe(async (answer) => {
if (this.id !== answer.to.id) return;
if (this.id === answer.from.id) return;
const peerConnection = this.connections.find((c) => c.id === answer.from.id);
if (!peerConnection) return;
await peerConnection.connection
.setRemoteDescription(answer.answer as RTCSessionDescriptionInit);
});
this.candidated$.subscribe(async (candidate) => {
if (this.id !== candidate.to.id) return;
if (this.id === candidate.from.id) return;
const peerConnection = this.connections.find((c) => c.id === candidate.from.id);
if (!peerConnection) return;
await peerConnection.connection.addIceCandidate(candidate.candidate as RTCIceCandidate);
});
}
private triggerConnection(channel: Channel) {
of(null)
.pipe(delay(500))
.subscribe(async () => {
await this.mutationLink({
channel: { id: channel.id },
participant: {
id: this.id,
},
});
});
}
private mutationLink(args: MutationLinkArgs) {
return this.client.mutate<
{ link: Channel },
MutationLinkArgs
>({
mutation: MUTATION_LINK,
variables: args,
});
}
private mutationOffer(args: MutationOfferArgs) {
return this.client.mutate<
{ offer: boolean },
MutationOfferArgs
>({
mutation: MUTATION_OFFER,
variables: args,
});
}
private mutationAnswer(args: MutationAnswerArgs) {
return this.client.mutate<
{ answer: boolean },
MutationAnswerArgs
>({
mutation: MUTATION_ANSWER,
variables: args,
});
}
private mutationCandidate(args: MutationCandidateArgs) {
return this.client.mutate<
{ candidate: boolean },
MutationCandidateArgs
>({
mutation: MUTATION_CANDIDATE,
variables: args,
});
}
private subscriptionLinked(args: SubscriptionLinkedArgs) {
return this.client.subscribe<
{ linked: ChannelWithParticipant },
SubscriptionLinkedArgs
>({
query: SUBSCRIPTION_LINKED,
variables: args,
});
}
private subscriptionOffered(args: SubscriptionOfferedArgs) {
return this.client.subscribe<
{ offered: Offer },
SubscriptionOfferedArgs
>({
query: SUBSCRIPTION_OFFERED,
variables: args,
});
}
private subscriptionAnswered(args: SubscriptionAnsweredArgs) {
return this.client.subscribe<
{ answered: Answer },
SubscriptionAnsweredArgs
>({
query: SUBSCRIPTION_ANSWERED,
variables: args,
});
}
private subscriptionCandidate(args: SubscriptionCandidatedArgs) {
return this.client.subscribe<
{ candidated: Candidate },
SubscriptionCandidatedArgs
>({
query: SUBSCRIPTION_CANDIDATED,
variables: args,
});
}
private async createRTCPeerConnectionAndSetupSendChannel(
type: PeerConnectionType, args: MutationLinkArgs | MutationOfferArgs,
) {
const peerConnection = new RTCPeerConnection();
let sendChannel: RTCDataChannel;
if (type === PeerConnectionType.OFFER) {
const { participant } = args as MutationLinkArgs;
sendChannel = peerConnection.createDataChannel(participant.id);
this.connection$.next({ id: participant.id, connection: peerConnection });
} else if (type === PeerConnectionType.ANSWER) {
const { from } = args as MutationOfferArgs;
sendChannel = peerConnection.createDataChannel(from.id);
this.connection$.next({ id: from.id, connection: peerConnection });
}
peerConnection.onicecandidate = async (e) => {
const { candidate } = e;
if (!candidate) return;
if (type === PeerConnectionType.OFFER) {
const { participant, channel } = args as MutationLinkArgs;
await this.mutationCandidate({
channel: { id: channel.id },
from: {
id: this.id,
},
to: { id: participant.id },
candidate: candidate as TransferRtcIceCandidateInput,
});
return;
}
if (type === PeerConnectionType.ANSWER) {
const { channel, from, to } = args as MutationOfferArgs;
await this.mutationCandidate({
channel: { id: channel.id },
from: { id: to.id },
to: { id: from.id },
candidate: candidate as TransferRtcIceCandidateInput,
});
}
};
peerConnection.ondatachannel = (e) => {
const { channel } = e;
if (!channel) return;
this.receiveChannel$.next(channel);
};
this.sendChannel$.next(sendChannel);
return peerConnection;
}
private enableReceivedChannels() {
this.receiveChannels.forEach((channel) => {
// eslint-disable-next-line no-param-reassign
channel.onmessage = (ev) => {
const { data } = ev;
if (!data) return;
try {
this.message$.next(JSON.parse(data) as ChannelMessage);
} catch {
this.message$.next(data as ChannelMessage);
}
};
});
}
}
Copy the code
The last part of the client code construction is the client code construction, which is used in practice:
webpack
ts-loader
fork-ts-checker-webpack-plugin
The final WebPack configuration is as follows:
/* node modules import */ const path = require('path'); /* webpack plugins */ const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); /** * @description webpack build config ( for typescript ) */ const config = { mode: 'none', entry: './src/index.ts', devtool: 'inline-source-map', target: 'web', output: { path: path.resolve(__dirname, 'lib'), filename: 'index.js', libraryTarget: 'umd', }, module: { rules: [ { test: /.ts?$/, use: [ { loader: 'ts-loader', }, ], exclude: /node_modules/, }, ], }, resolve: { extensions: ['.ts', '.js'], }, // webpack4 optimizations optimization: { nodeEnv: false, minimize: true, }, // to let __dirname & __filename not a relative path node: { __filename: false, __dirname: false, }, // fork tsconfig.json plugins: [ new ForkTsCheckerWebpackPlugin({ typescript: { configFile: path.resolve(__dirname, 'tsconfig.json'), }, }), ], externals: [// Apollo client relies on React but is not SDK dependent, so it is excluded from the build list 'react',],}; export default config;Copy the code
Video call
This part of the logic is not covered yet, but following the current design, the implementation is actually a change of the same, if you are interested in studying the code further, can try to complete this part of the logic.
conclusion
WebRTC creates a lot of complex apis for developers, which can be a pain in the neck. Considering GraphQL and Typescript to standardize our code at this point would be a lot easier.
If you like my share, welcome to follow me! Stay at the end, my Wechat: ohkuku