открывалка
Клавиатура недавно сломалась, и я только что увидел техническое эссе Nuggets Audio Network и подумал о всей клавиатуре. Поэтому я начал изучать webrtc с нуля, При первом чтении документ получает тройку. Это так сложно, как это исправить, кто выдержит? Поэтому я начал искать информацию по всему Интернету, и мне повезло найти ее на Наггетс.Сериал webrtc о сумасшедшем боссе Цзян Сан,так же какУчебник по серии сообщений WebRTC в реальном времени, или оригинальная английская версияReal time communication with WebRTC, Заинтересованные студенты также могут посмотреть, это очень хорошо. Так как есть такая замечательная статья, почему вы хотите написать еще одну статью?Конечно, чтобы поделиться (чжэн) наслаждаться (гэ) опытом (цзянь) опытом (пань). Ввиду того, что на изучение и добавление проектов у меня ушло почти три недели, проект писался и писался в тысячу строк.Хотя было что-то в середине, что задержало на время, но и стоило мне много сил и ступил на бесчисленные Ямы, здесь я постараюсь начать с самого простого и использовать простой и понятный способ, чтобы привести всех к завершению более полной картины, я думаю. Статья может быть очень длинной, читать можно медленно. Некоторые пункты знаний не нуждаются в такой детализации.Для того, чтобы сделать ваше мышление более ясным, введение будет опущено.Если вам интересно, вы можете прочитать его самостоятельно.
Демонстрация проекта
Адрес на гитхабе:ты рисуешь я думаю
Добро пожаловать Звезда!
webrtc
WebRTC (веб-связь в реальном времени) — это API, который можно использовать в веб-приложениях, таких как видеочат, аудиочат или совместное использование файлов P2P.
Полное название — веб-связь в реальном времени.Из официальной документации видно, что его можно использовать для видео-чата, аудио-чата, end-to-end (p2p), передачи данных и обмена файлами. Эта технология в настоящее время используется в прямых трансляциях.
В webrtc есть три важных API, которые соответствуют трем функциям.
- getUserMedia запрашивает получение мультимедийной информации пользователя, включая видеопоток (видео) и аудиопоток (аудио).
- RTCPeerConnection представляет собой WebRTC-соединение локального компьютера с удаленным, используемое для обеспечения сквозного соединения. Этот интерфейс предоставляет реализации методов для создания, обслуживания, мониторинга и закрытия соединений.
- RTCDataChannel представляет собой установление двунаправленного соединения канала данных между ними, которое является каналом данных, который передает данные
getUserMedia
Прежде всего, мы сначала реализуем простой сбор видео и аудио и отображаем его на веб-странице.
javasrcipt
// 获取本地的视频和音频流,{ audio: true, video: true }都是true这两个都获取
let localStream
navigator.mediaDevices.getUserMedia({ audio: true, video: true })
.then((stream) => {localStream = stream})
//找到video标签,用一个video来接受流,并且显示
let video = document.querySelector("#video")
// 使用srcObject给video添加流
video.srcObject = localStream
html
<video id="video" autoplay style="width:600; height:400;"></video>
Поскольку здесь нам нужно только получить поток данных, мы не будем здесь подробно объяснять API, мы можем перейти к официальной документации.MDN. Отсюда мы видим, что нам нужен только простой API для получения локального видео и аудио потока, нам наконец нужно отправить этот поток другим клиентам, как отправить поток, мы используем RTCPeerConnection для подключения и передачи потока.
navigator.getUserMedia в настоящее время все еще поддерживается. Однако в официальной документации он устарел, и следует использовать getUserMedia() для navigator.MediaDevices, но этот API в настоящее время поддерживается не всеми браузерами, и есть проблемы с совместимостью.
Чтобы избежать проблем с совместимостью, мы можем использовать следующий код для адаптации совместимости
//浏览器不支持navigator.mediaDevices
if (navigator.mediaDevices == undefined) {
navigator.mediaDevices = {}
navigator.mediaDevices.getUserMedia = function (constraints) {
//获得旧版的getUserMedia
let getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia
//浏览器就不支持getUserMedia这个api,则返回个错误
if (!getUserMedia) {
return Promise.reject(new Error('getUserMedia is can not use in the browser'))
}
// getUserMedia是异步的,所以用Promise,将返回一个绑定在navigator上的getUserMedia
return new Promise((resolve, reject) => {
getUserMedia.call(navigator, constraints, resolve, reject)
})
}
}
RTCPeerConnection
Это самый важный API для реализации сквозного (без обмена данными через сервер) соединения, и это также самая сложная часть для понимания.
Сквозное соединение нужно подключить через сервер в первый раз, и сервер нужно ретранслировать После первого подключения нет необходимости проходить через сервер. Здесь мы используем socket.io и немного koa, о которых поговорим позже. Есть и другие способы, о которых мы здесь не будем рассказывать, кому интересно, могут почитать статьи Цзян Санмада. Короче говоря, впервые для реализации соединения на обоих концах требуется сервер.
Далее идет конкретный процесс обмена
- Создайте экземпляр RTCPeerConnection
- Обменивайтесь описаниями данных локального и удаленного sdp, используйте предложения и ответы для проникновения через nat и устанавливайте p2p.
- Обмен информацией о ледяной сети для обмена сетевой информацией при работе в сети
Создайте экземпляр RTCPeerConnection
let PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection
let peer = new PeerConnection(iceServers)
Есть параметр iceServers, а в параметре два атрибута, а именно оглушение и поворот. Используется для проникновения через NAT.Подробнее см.WebRTC in the real world: STUN TURN and signaling
{
iceServers: [
{ url: "stun:stun.l.google.com:19302"}, // 谷歌的公共服务
{
url: "turn:***",
username: ***, // 用户名
credential: *** // 密码
}
]
}
NAT
Давайте поговорим о том, почему мы используем технологию проникновения NAT для достижения однорангового соединения.
Полное имя NAT (преобразование сетевых адресов) используется для обмена сетевыми адресами, что приведет к тому, что мы не получим реальный IP-адрес устройства.
Поскольку внешняя сеть использует коды адресов IPV4, количество кодов адресов недостаточно, поэтому устройства NAT, такие как маршрутизация, будут использоваться для изменения IP-адреса и номера порта внешней сети и использования адресов IPV6, чтобы несколько внутренних сетей может быть внешняя сеть. Это увеличивает количество сетевых подключений, но делает невозможным непосредственное обнаружение интрасети другой стороны из интрасети, поэтому нам нужно выполнить проникновение NAT для достижения сквозных подключений.
Общие шаги проникновения через NAT следующие: A, B заканчиваются, сегмент A отправляет сообщение в конец B, это сообщение будет отброшено устройством NAT, но оставит дыру в NAT, в следующий раз, когда сообщение может пройти через Это отверстие используется для передачи Аналогично, B также отправляет сообщение, чтобы пройти через свое собственное устройство NAT. Конкретная реализация использует STUN и TURN для выполнения проникновения NAT.Процесс заключается в выполнении проникновения NAT через сервер STUN.Если он не может быть проникнут, необходимо использовать сервер TURN для транзита.Как проникнуть можно увидеть.Реализация обхода NAT по протоколу ICE (STUN&TURN), и мы можем построить свои собственные STUN и TURN,Создайте свой собственный сервер WebRTC TURN&STUN
- STUN (простой обход протокола пользовательских дейтаграмм через трансляторы сетевых адресов (NAT), UDP Simple Traversal of NAT) — это сетевой протокол.
- Полное название TURN — Traversal Using Relay NAT. Протокол TURN позволяет объектам, находящимся за NAT или брандмауэрами, получать данные через TCP или UDP.
P2P
Теперь, когда мы разобрались с обходом NAT, давайте воспользуемся PeerConnection для реализации p2p-соединения. Выше мы создали экземпляр PeerConnection, назовем его localPeer, remotePeer. Теперь давайте обменяемся описаниями локальных и удаленных данных sdp, сначала код.
localPeer.createOffer()
.then(offer => localPeer.setLocalDescription(offer))
.then(() => remotePeer.setRemoteDescription(localPeer.localDescription))
.then(() => remotePeer.createAnswer())
.then(answer => remotePeer.setLocalDescription(answer))
.then(() => localPeer.setRemoteDescription(remotePeer.localDescription))
Реализация обмена описаниями локальных и удаленных данных sdp аналогична нашим предыдущим шагам для обхода NAT.
- локальный одноранговый вызов
createOffer()
api для создания предложения типа sdp и использованияsetLocalDescription()
добавить это вlocalDescription
, тут просто устанавливаем p2p локально, сервер не нужен, для первого подключения - remotePeer принимает localPeer
localDescription
и использоватьsetRemoteDescription
добавить его к себеRemoteDescription
- remotePeer через
createAnswer()
Создайте sdp типа ответа и добавьте его в свой собственныйLocalDescription
- localPeer будет удаленным
localDescription
добавить как собственныйremoteDescription
На данный момент обмен данными sdp между двумя концами завершен, что означает, что локальный p2p был подключен, но мы создали два конца на одном и том же интерфейсе, что не является истинным p2p.Если мы хотим использовать p2p сети, нам необходимо использовать лед для реализации однорангового соединения сети, а также нужен socket.io для установления первой передачи данных
SDP
SDP (протокол описания сеанса, протокол описания сеанса). Он не относится к транспортному протоколу, но может использовать различные транспортные протоколы, включая протокол уведомления о сеансе (SAP), протокол инициации сеанса (SIP), протокол потоковой передачи в реальном времени (RTSP). ), электронную почту MIME Extension Protocol и протокол передачи гипертекста (HTTP).
Это конкретный sdp, который представляет собой метаданные локального носителя, вы можете перейти к деталямСтандартный протокол связи P2P (3) ICE
v=0
o=- 1877521640243013583 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0 1 2
a=msid-semantic: WMS
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
Давайте еще раз посмотрим на предложение
Вы можете видеть, что предложение является сдп типа предложения, и ответ тот же
ICE
Полное название ICE — Interactive Connectivity Establishment, то есть интерактивное установление соединения. ICE — это транспортный протокол NAT, используемый в режиме предложения/ответа, в основном используемый для установления мультимедийных сеансов по протоколу UDP с использованием протокола STUN и TURN. протокол
Если нам нужно реализовать p2p сети, нам нужно подключить ледовый протокол на обоих концах. Здесь нам нужно использовать
-
RTCPeerConnection.onicecandidate()
API используется для мониторинга изменений локальной сети ice, и если она есть, она будет отправлена через socket.io, -
RTCPeerConnection.addIceCandidate()
Используется для добавления полученного льда в локальный экземпляр RTCPeerConnection.
транспортный потокКогда p2p установлен, мы можем использовать экземпляр RTCPeerConnection в
- addstream() добавляет локальный медиапоток,
- onaddstream() обнаруживает локальный медиапоток,
onaddstream() будет выполняться сразу после выполнения setRemoteDescription ответа получателя, что означает, что мы не можем использовать addstream для добавления потока после завершения создания p2p.
addstream() и onaddstream() устарели в официальной документации, лучше использовать обновленные addTrack() и onaddTrack(), вы можете посмотреть MDN, если вам интересно
RTCDataChannel
RTCDataChannel используется для канала данных в p2p, мы используем RTCPeerConnectioncreateDataChannel()
для создания экземпляра TCDataChannel. Здесь мы предполагаем, что экземпляр с именем channel создан, и API, которые нам здесь нужны, это
- Канал channel.send() активно отправляет данные в подключенный канал
- OnDatachannel () для мониторинга того, изменен ли канал, например OPEN (ONOPEN), BLACK (ONCLOSE), чтобы получить данные (OnMessage) Отправить
//发送数据hello
channel.send(JSON.stringify('hello'))
// 监听channel的状态
peer.ondatachannel = (event) => {
var channel = event.channel
channel.binaryType = 'arraybuffer'
channel.onopen = (event) => { // 连接成功
console.log('channel onopen')
}
channel.onclose = function(event) { // 连接关闭
console.log('channel onclose')
}
channel.onmessage = (event) => { // 收到消息
let data = JSON.parse(event.data)
console.log('channel onmessage', data)
}
}
Здесь была написана наша основа WebRTC.Хотя WebRTC - это P2P, для которого не требуется сервер, наше первое соединение - это сервер, который помогает нам найти ответ на ответ, таким образом взаимодействуя ПРЕДЛОЖЕНИЕ, ОТВЕТ, ICE и другую информацию, Создайте P2P связь. Далее мы впервые используем KOA и Socket.io в качестве сервера, а также некоторые взаимодействия бизнес-логики.
koa&socket.io
koa
koa — это промежуточный фреймворк для службы HTTP. Он очень легкий и практически не имеет интеграции. Для использования многих функций требуется установка подключаемых модулей. И используется синтаксис es6, и для достижения асинхронности используется асинхронность.
Нам нужно создать server.js для развертывания сервера.
import Koa from 'koa'
import { join } from 'path'
import Static from 'koa-static'
import Socket from 'socket.io'
// 创建一个socket.io
const io = new Socket({
options : {
pingTimeout: 10000,
pingInterval: 5000
}
})
// 创建koa
const app = new Koa()
// socket注入app
io.attach(app)
// 添加指定静态web文件的Static路径
// Static(root, opts) 这里将public作为根路径
app.use(Static(
// join 拼接路径
// __dirname返回被执行文件夹的绝对路径
join( __dirname, './public')
))
// 服务器端口号,这里两个listen外面的是socket.io的,后面一个是koa的listen,需要将socket监听koa的端口,不然会报错
io.listen(app.listen(3000, () => {
console.log('server start at port: ' + 3000)
}))
socket.io
Давайте сначала познакомимся с сетевым протоколом WebSocket, который отличается от протокола http.Подробнее см.websocket
Socket.io — это сетевой протокол WebSocket, который использует сервер, который является новым коммуникационным протоколом в HTML 5. Его особенностью является то, что сервер может активно передавать информацию клиенту, а клиент также может активно отправлять информацию на сервер. настоящий двусторонний равноправный диалог — это разновидность серверной технологии push.
Таким образом, мы можем реализовать взаимодействие через активную отправку двух концов к серверу и активную отправку сервера к двум концам. Нам нужно использовать API socket.io
- socket.on('event', () => {}) слушать события, вызванные сокетом
- socket.emit('event', () => {}) Активно отправлять
- socket.join('room', () => {}) присоединиться к комнате
- socket.leave('room', () => {}) выйти из комнаты
- socket.to(room | socket.id) | socket.in(room | socket.id) указывает комнату или сервер
Сначала клиент и сервер подключаются друг к другу. Поскольку номер порта на стороне сервера установлен на 3000, сервер сокетов на нашей html-странице
// html
// 引入
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
// 连接3000端口
var socket = io('ws://localhost:3000/')
// server.js
// 监听连接
// io是服务器端的, socket是客户端的
io.on('connection', socket => {
...
})
// 监听关闭
io.on('disconnect', socket => {})
Реализуем первое подключение webrtc через сокет
// A 向 B 的p2p
// html
// A
// user 是全局变量,存在sessionStorage中, 创建时候获取
var user = window.sessionStorage.user || ''
// 发给服务器改socket的名称
socket.emit('createUser', 'A')
// 兼容性
let PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection
var peer = new PeerConnection()
// 创建A端的offer
peer.createOffer()
.then(offer => {
// 设置A端的本地描述
peer.setLocalDescription(offer, () => {
// socket发送offer和房间
socket.emit('offer', {offer: offer, user: 'B'})
})
})
// 监听本地的ice变化,有则发送个B
peer.onicecandidate = (event) => {
if (event.candidate) {
![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/6/3/16b1b606f637e98e~tplv-t2oaga2asx-image.image)
// B
// user 是全局变量,存在sessionStorage中, 创建时候获取
var user = window.sessionStorage.user || ''
// 发给服务器改socket的名称
socket.emit('createUser', 'A')
let PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection
var peer = new PeerConnection()
// 接受服务器端发过来的offer辨识的数据
socket.on('offer', date => {
// 设置B端的远程offer 描述
peer.setRemoteDescription(data.offer, () => {
// 创建B的Answer
peer.createAnswer()
.then(answer => {
// 设置B端的本地描述
peer.setLocalDescription(answer, () => {
socket.emit('answer', {answer: answer, user: 'A'})
})
})
})
})
socket.on('ice', data => {
// 设置B ICE
peer.addIceCandidate(data.candidate);
})
socket.emit('createUser', 'B')
// server.js
// 用于接受客户端的用户名对应的服务器
const sockets = {}
// 保存user
const users = {}
io.on('connection', data => {
// 创建账户
socket.on('createUser', data => {
let user = new User(data)
users[data] = user
sockets[data] = socket
})
socket.on('offer', data => {
// 通过B的socket的id只发送给B
socket.to(sockets[data.user].id).emit('offer', data)
})
socket.on('answer', data => {
// 通过B的socket的id只发送给A
socket.to(sockets[data.user].id).emit('answer', data)
})
socket.on('ice', data => {
// ice发送给B
socket.to(sockets[data.user].id).emit('ice', data)
})
})
Выше приведено первое соединение для достижения p2p через socket.io. Это то же самое, что и наш процесс на основе webrtc, за исключением того, что для передачи используется server.js. В последующей бизнес-логике нам необходимо осуществлять широковещательную рассылку на множество различных групп серверов.Здесь мы расширяем типы широковещательных сокетов.
- io.emit() транслирует всем клиентам, подключенным к серверу, например, отображает информацию о комнате
- io.to(room).emit() транслирует всем клиентам в комнате уведомления в комнате
- socket.to(room).emit() отправляет сервер в комнату, отличную от той, которую он считает
- socket.emit() отправляет на сам сервер
- socket.to(socket.id).emit() отправить на указанный сервер
Здесь мы узнали об использовании некоторых наших API на socket.io и использовании socket.io для реализации p2p, Далее мы реализуем чертежную доску на холсте.
canvas
Cnavas — это доска для рисования в html5, мы можем использовать ее для реализации функции рисования в html, здесь наша доска для рисования тоже сделана с этим. Чтобы реализовать чертежную доску, мы используем класс для ее инкапсуляции, и нам нужно реализовать следующие функции.
- кисть для рисования узоров
- ластик, четкий узор
- вернуться, вернуться к предыдущему рисунку
- вперед, вперед к следующей картине
- Очистить, очистить все шансы на покраску
- Установите строку, используемую для установки ширины кисти и ластика
- установить цвет, используется для установки цвета кисти
- Функция операции, используемая для вызова разных функций в соответствии с разными операциями.
- Функция обратного вызова, используемая для обратного вызова событий, используемая для передачи данных и синхронизированной чертежной доски
Итак, мы можем написать наш класс рисования холста
// 创建绘图类
class Draw {
constructor(canvas, callBack) {
this.canvas = canvas
this.ctx = canvas.getContext('2d')
this.width = this.canvas.width
this.height = this.canvas.height
this.color = color
this.weight = weight
this.isMove = false
this.option = ''
// 保存每次鼠标按下并抬起的所绘制的图片,用于撤回,前进
this.imgData = []
// 记录当前帧
this.index = 0
// 现在的坐标
this.now = [0, 0]
// 移动前的坐标
this.last = [0, 0]
this.bindMousemove = this.onmousemove.bind(this)
this.callBack = callBack || function() {}
}
// 初始化
init() { }
// 监听鼠标按下
onmousedown(event) { }
// 监听鼠标移动
onmousemove(event) { }
// 监听鼠标抬起
onmouseup() { }
//绘制线条
line(last, now, weight, color) { }
// 橡皮
eraser(last, now, weight) { }
// 回退
back() { }
// 前进
go() { }
// 清除
clear() { }
// 收集每一帧的图片
getImage() { }
// 绘制当前帧的图片
putImage() { }
// 设置尺寸
setWeight(weight) { }
// 设置颜色
setColor(color) { }
// 所有的操作的合集
options(option, data) { }
}
Давайте подробно реализуем эти методы
Коллекция операций
options(option, data) {
switch (option) {
case 'pen': {
this.line(...data)
this.callBack('pen', data)
break
}
case 'eraser': {
this.eraser(...data)
this.callBack('eraser', data)
break
}
case 'getImage': {
this.callBack('getImage')
this.getImage()
break
}
case 'go': {
this.callBack('go')
this.go()
break
}
case 'back': {
this.callBack('back')
this.back()
break
}
case 'clear': {
this.callBack('clear')
this.clear()
break
}
case 'setWeight': {
this.callBack('setWeight', data)
this.setWeight(data)
break
}
case 'setColor': {
this.callBack('setColor', data)
this.setColor(data)
break
}
}
}
Здесь мы помещаем вызовы всех операций в один метод, что способствует рефакторингу кода, но основная цель этого заключается в том, что когда мы пишем функцию обратного вызова каждой операции в методе option, а не в методе конкретной операции, это может избежать того, что когда мы используем функцию обратного вызова для передачи параметров, получатель будет использовать этот метод для обновления своего собственного холста, а затем вызовет обратный вызов, что приведет к бесконечным обратным вызовам на обоих концах.
Кисти и ластики
Идея нашей реализации кисти заключается в том, что при нажатии мыши мы отслеживаем движение мыши, а при движении мыши в функцию опций передается параметр положения мыши. это кисть или ластик через this.option и вызывает соответствующую функцию. При поднятии мыши отслеживание события движения завершается, текущий кадр сохраняется и вызывается функция обратного вызова для передачи информации о сохраненной игле.
onmousedown(event) {
this.last = [event.offsetX, event.offsetY]
this.canvas.addEventListener('mousemove', this.bindMousemove)
}
onmousemove(event) {
this.isMove = true
this.now = [event.offsetX, event.offsetY]
let data = [
this.last,
this.now,
this.weight,
this.color
]
this.options(this.option, data)
}
onmouseup() {
this.canvas.removeEventListener('mousemove', this.bindMousemove)
if (this.isMove) {
this.isMove = false
this.options('getImage')
}
}
line(last, now, weight, color) {
this.ctx.beginPath()
this.ctx.lineCap = 'round'
this.ctx.lineJoin = 'round'
this.ctx.lineWidth = weight
this.ctx.strokeStyle = color
this.ctx.moveTo(last[0], last[1])
this.ctx.lineTo(now[0], now[1])
this.ctx.closePath()
this.ctx.stroke()
this.last = now
}
eraser(last, now, weight) {
this.ctx.save()
this.ctx.beginPath()
// console.log(now[0] , now[1])
this.ctx.arc(now[0], now[1], weight, 0, 2 * Math.PI)
this.ctx.closePath()
this.ctx.clip()
this.ctx.clearRect(0, 0, this.width, this.height)
this.ctx.fillStyle = '#fff'
this.ctx.fillRect(0, 0, this.width, this.height)
this.ctx.restore()
}
Конкретная реализация кисти
- ctx.beginPath() означает начать отрисовку пути и задать характеристики, цвет и т. д. нижней линии.
- ctx.moveTo(last[0], last[1]) означает переместить положение пера в первое положение, что означает фактическое положение кисти.
- ctx.lineTo(now[0], now[1]) означает провести линию от (last[0], last[1]) до (now[0], now[1]).
- this.ctx.closePath() закрывает рисунок пути
- ctx.stroke() использует линии для рисования, а не заполнения
- last = теперь обновить точку координат
Конкретная реализация резины
- ctx.save() сохраняет текущее состояние
- ctx.beginPath() начинает рисовать путь
- ctx.arc(now[0], now[1], weight, 0, 2 * Math.PI) рисует окружность, параметры — центр x, y, радиус r, а также начальный и конечный углы. Здесь начальный угол 0 от положительной оси оси x, один круг. Это эквивалентно рисованию круга в конечной позиции смещения мыши.
- ctx.closePath() закрывает рисунок пути
- ctx.clip() — еще один метод рисования пути. Он обрезает путь, который мы рисуем, так что все наши последующие операции будут в этой области рисования пути. Используйте clip, чтобы нарисовать путь, который должен быть закрытым путем.
- ctx.clearRect(0, 0, this.width, this.height) Хотя здесь очищается весь экран, поскольку мы используем клип для рисования пути, все наши действия будут действовать только в области клипа, так что мы очищаем только наша Нарисованная область, то есть область, стертая ластиком
- ctx.fillStyle = '#fff' ctx.fillRect(0, 0, this.width, this.height) заполнить очищенную область белым цветом
- ctx.restore() перерисовывает ранее сохраненный артборд, остальные места не меняются, меняются только стертые места.
Подробнее можно посмотретьХолст рисовать формы
вперед и назад
Вперед и назад каждый раз, когда мышь поднимается, мы считаем стежок через холст.
- this.ctx.getImageData(0, 0, this.width, this.height) Параметры (x, y, ширина, высота) Здесь мы делаем скриншот всего холста, чтобы получить изображение, и сохраняем его в this.imgData = [] в массиве
- Укажите текущий кадр по this.index, index++ для прямого хода, reverse для обратного
- Выпустить изображение текущего кадра через this.ctx.putImageData(this.imgData[this.index], 0, 0), перед использованием необходимо очистить экран
очистить, задать параметры
- this.imgData = [] очистить массив изображений
- this.ctx.clearRect(0, 0, this.width, this.height) очистить экран
- this.index = 0 очищает указатель
- this.getImage() сохраняет первый контакт
- this.weight = вес устанавливает ширину шрифта
- this.color = цвет загрузки цвета
Пока что технология, используемая в нашем холсте, внедрена.
один ко многим, многие ко многим
Есть несколько режимов видео, вы можете перейти квидео режим, разные режимы обрабатывают разные ситуации, но здесь мы используем соединения «многие ко многим p2p». Поскольку это p2p, для достижения многих ко многим он может стать один к одному для каждого. Это соединение p2p через каждый конец. Здесь нужно обратить внимание на порядок добавления. Здесь мы имеем то, что, когда кто-то входит в комнату, человек, который входит, и комната выполняют p2p, а человек, который вошел, выполняет p2p только с вошедшим человеком. Таким образом, это все p2p
// nat连接方法
function createPeers(data) {
if (user !== data.joinUser) {
let conn = [data.joinUser, user].join('-')
if (!peers[conn]) {
initPeer(conn)
}
} else if (data.joinUser === user) {
if (data.roomusers.length > 1) {
data.roomusers.forEach(roomuser => {
if (roomuser.name !== user) {
let conn = [data.joinUser, roomuser.name].join('-')
if (!peers[conn]) {
// initPeer和之前差不多,就多了将新建的Peer和channel加入数组
initPeer(conn)
}
}
})
}
}
}
Мы используем массив на каждом клиенте для хранения. Разные p2p отмечаются маркировкой добавленных и существующих пользователей.
Конкретная реализация каждого p2p
И перед тем же индивидуумом, но мы пройдём за цикл по массиву, люди в каждой комнате предложат отправить
// 新建对每个已经在房间的offer
if (data.joinUser === user) {
for (let conn in peers) {
// conn标示
createoffer(conn, peers[conn])
}
}
function createoffer(conn, peer) {
peer.createOffer({
offerToReceiveAudio: 1,
offerToReceiveVideo: 1
})
.then(offer => {
peer.setLocalDescription(offer, () => {
console.log('setLocalDescription-offer', peer.localDescription)
socket.emit('offer', {room: room, conn: conn, user: conn.split('-')[0], toUser: conn.split('-')[1], sdp: offer})
})
})
}
При использовании Socket.IO Чтобы сделать первое подключение, соответствующая передача должна выполняться через Conn Mark. Мы разделяем CHN, пользователь является отправителем, а Touser является приемником.
// 转发offer
socket.on('offer', data => {
// 通过toUser发送个其对应的socket
socket.to(sockets[data.toUser].id).emit('offer', data)
})
// 接收端收到offer
socket.on('offer', (data) => {
console.log('setRemoteDescription-offer-sdp', data.conn, data.sdp)
var peer = peers[data.conn]
peer.setRemoteDescription(data.sdp, () => {
peer.createAnswer()
.then(answer => {
peer.setLocalDescription(answer, () => {
console.log('setLocalDescription-answer', data.conn, answer)
// 此时将发送者和接受者互换,发送answer
socket.emit('answer', {room: room, user: data.toUser, toUser: data.user, conn: data.conn, sdp: answer})
})
})
})
})
// 转发answer
socket.on('answer', data => {
socket.to(sockets[data.toUser].id).emit('answer', data)
})
// 请求端收到answer
socket.on('answer', (data) => {
// 呼叫端设置远程 answer 描述
var peer = peers[data.conn]
peer.setRemoteDescription(data.sdp, () => {
console.log('setRemoteDescription-answer-sdp', data.conn, data.sdp)
})
})
добавить лед
// 监听ICE候选信息 如果收集到,就发送给对方
peer.onicecandidate = (event) => {
if (event.candidate) {
socket.emit('ice', {room: room, conn: conn, user: conn.split('-')[0], toUser: conn.split('-')[1], candidate: event.candidate})
}
}
// 转发iceCandidate
socket.on('ice', data => {
socket.to(sockets[data.toUser].id).emit('ice', data)
})
// 收到Ice
socket.on('ice', (data) => {
console.log('onice', data.conn, data.candidate)
var peer = peers[data.conn]
console.log('------------------------peer',peer)
peer.addIceCandidate(data.candidate); // 设置远程 ICE
})
Вот и закончился наш p2p
Динамический эффект монтажной области
Здесь у нас есть три метода:
- Активная передача данных осуществляется через socket.io, но у нас тоже обычный метод one-to-many. Но так как на этот раз мы используем webrtc, мы не будем использовать этот метод.
- По холсту становится поток данных, и осуществляется через addStream и onAddStream, поток потоков и принимается видео, но здесь есть яма, потому что яма застряла на неделю, потому что нам нужно изменить, чтобы добавить объект потока, но прежде, чем мы скажем, onaddstream() будет выполняться сразу после окончания ответа шаттла setRemoteDescription выполнено, поэтому мы не можем завершить соединение после переключения объекта потока, поэтому этот метод не работает в моем запросе
- Он реализован через RTCDataChannel.Этот метод очень похож на первый метод.Принцип заключается в том, чтобы активно отправлять данные на другие терминалы, а другие терминалы могут рисовать на своем собственном холсте.Так как мы используем этот метод, давайте представим его сейчас. Конкретный процесс реализации
Как упоминалось ранее, в классе холста есть функция обратного вызова, и при работе будет вызвана функция обратного вызова, и параметры будут переданы в метод sendOther() вне класса.
- sendOther(option, data) передает два параметра: один — операция option, соответствующая разным методам, а data data — данные метода
- channels[conn].send(JSON.stringify(data)) channels[conn] Соответствующий канал в массиве, мы можем использовать цикл for для активной отправки данных всем подключенным p2p.
- Принимающая сторона ondatachannel будет принимать отправленные данные и работать в соответствии с различными вариантами.
peer.ondatachannel = (event) => {
var channel = event.channel
channel.binaryType = 'arraybuffer'
channel.onopen = (event) => { // 连接成功
console.log('channel onopen')
}
channel.onclose = function(event) { // 连接关闭
console.log('channel onclose')
}
channel.onmessage = (event) => { // 收到消息
let obj = JSON.parse(event.data)
let option = obj.option
let data = obj.data
// console.log('onmessage----------', data, option, event)
if (option === 'text') {
msgList.push(data)
updateMsgList(data)
} else {
switch (option) {
case 'pen': {
draw.line(...data)
break
}
case 'eraser': {
draw.eraser(...data)
break
}
case 'getImage': {
draw.getImage()
break
}
case 'back': {
draw.back()
break
}
case 'go': {
draw.go()
break
}
case 'clear': {
draw.clear()
break
}
case 'setWeight': {
draw.setWeight(...data)
break
}
case 'setColor': {
draw.setColor(...data)
break
}
}
}
// console.log('channel onmessage', e.data);
}
}
Суммировать
В этом проекте все еще есть много приростов. Прежде всего, это поле WEBRTC. Если бы это не было для этого проекта, я мог бы не был в этой области. Он также укрепил мою возможности лохматов и бизнес-логиков. Письменный бизнес с родным js действительно хлопот. Поскольку я писал небольшую программу в течение этого времени, некоторые части этого проекта все еще не идеальны, а какая-то деловая логика еще не была написана, но основные функции были написаны, и оно не оказывает большого влияния.
Agora SDK Use Experience Essay Contest | Эссе о технологиях Nuggets, работа над эссе продолжается