首页 > 其他分享 >【BUN】bun搭配 WebRTC 实现一个直播平台

【BUN】bun搭配 WebRTC 实现一个直播平台

时间:2024-12-10 18:42:33浏览次数:3  
标签:const data userId li BUN bun roomId WebRTC ws

前言:
近日。学习BUN中,突发奇想,如何实现一个直播平台?


0. BUN的安装

安装BUN

1. 初始化项目

bun init

2. 实现serve信令服务器

index.ts

import Bun from 'bun';
import type {ServerWebSocket} from 'bun';

type MessageKeys =
  'join'
  | 'create'
  | 'offer'
  | 'answer'
  | 'icecandidate'
  | 'error'
  | 'success'
  | 'leave'
  | 'close'
  | 'joined'
  | 'danmaku'
  | 'updateRooms'

class Share {
  constructor() {
  }

  port = 3000;

  messageHandlers: Partial<Record<
    MessageKeys,
    (ws: ServerWebSocket<string>, payload: Record<string, any>) => void
  >> = {
    join: (ws, payload) => {
      // 有用户加入房间
      const {roomId, userId, username} = payload;

      this.users.set(userId, {
        ws,
        name: username,
        roomId
      })

      if (!roomId) return;

      const room = this.rooms.get(roomId);

      if (!room) {
        this.sendMessage(ws, 'error', {
          message: '房间不存在或已关闭'
        })
        return
      }

      this.users.set(userId, {
        ws,
        name: username,
        roomId
      })

      room.clients.push(ws)

      this.sendMessage(room.host, 'joined', {
        userId,
        username
      })

      this.sendMessage(ws, 'success', {
        message: `加入房间 ${room.name} 成功`
      })

    },
    create: (ws, payload) => {
      const {roomId, roomName, cover} = payload;
      this.rooms.set(roomId, {
        host: ws,
        name: roomName,
        cover,
        clients: []
      })

      // 通知所有用户 有新房间
      for (const [userId, user] of this.users) {
        const {ws: userWs} = user;
        this.sendMessage(userWs, 'updateRooms', {
          roomId,
          type: 'create'
        })
      }

    },
    offer: (ws, payload) => {
      const {offer, userId, roomId} = payload
      const user = this.users.get(userId);
      if (!user) {
        this.sendMessage(ws, 'error', {
          message: '用户不存在'
        })
        return
      }
      const {ws: userWs} = user;
      this.sendMessage(userWs, 'offer', {
        offer,
        userId,
        roomId
      })
    },
    answer: (ws, payload) => {
      const {answer, userId, roomId} = payload
      const room = this.rooms.get(roomId);
      if (!room) {
        this.sendMessage(ws, 'error', {
          message: '房间不存在'
        })
        return
      }
      this.sendMessage(room.host, 'answer', {
        answer,
        userId,
        roomId
      })
    },
    icecandidate: (ws, payload) => {
      const {candidate, userId, roomId} = payload
      const user = this.users.get(userId);
      if (!user) {
        this.sendMessage(ws, 'error', {
          message: '用户不存在'
        })
        return
      }
      const {ws: userWs} = user;
      this.sendMessage(userWs, 'icecandidate', {
        candidate,
        userId,
        roomId
      })
    },
    danmaku: (ws, payload) => {
      const {roomId, admin, message, username, userId} = payload
      const room = this.rooms.get(roomId)
      if (!room) {
        this.sendMessage(ws, 'error', {
          message: '房间不存在或已关闭'
        })
        return
      }
      const vo = {
        admin,
        message,
        username,
        userId
      }
      for (const client of room.clients) {
        this.sendMessage(client, 'danmaku', vo)
      }
      const {host} = room;
      this.sendMessage(host, 'danmaku', vo)
    }
  }

  rooms = new Map<string, {
    host: ServerWebSocket<string>;
    name: string;
    cover: string;
    clients: ServerWebSocket<string>[];
  }>(); // 房间
  users = new Map<string, {
    name: string;
    ws: ServerWebSocket<string>;
    roomId: string;
  }>(); // 用户

  get roomData() {
    const data = []
    for (const [roomId, room] of this.rooms) {
      data.push({
        id: roomId,
        name: room.name,
        cover: room.cover
      })
    }
    return data
  }

  sendMessage(ws: ServerWebSocket<string>, type: MessageKeys, data: Record<string, any>) {
    ws.send(JSON.stringify({
      type,
      data
    }))
  }


  async start() {
    Bun.serve<string>({
      port: this.port,
      fetch: async (request, server) => {
        const path = new URL(request.url).pathname
        if (path === "/") {
          const file = Bun.file("./src/index.html")
          const exists = await file.exists();
          if (exists) return new Response(file)
        }
        if (path === '/ws') {
          const success = server.upgrade(request)
          if (success) return new Response("Upgrading...")
        }
        if (path.startsWith("/src/")) {
          const filePath = path.replace("/src/", "./src/");
          const file = Bun.file(filePath)
          const exists = await file.exists();
          if (exists) return new Response(file)
        } else if (path.startsWith('/api/')) {
          if (path === '/api/rooms') {
            return new Response(JSON.stringify(this.roomData))
          }
        }
        return new Response(path);
      },
      websocket: {
        open() {
        },
        close: (ws, code, reason) => {
          // 用户离开 或者 关闭房间
          // 判断是用户离开还是关闭房间
          // 1. 如果从用户列表中找到用户,说明是用户离开 则需要通知房间内的其他用户 和 房主
          // 2. 如果从房间列表中找到房间,说明是房主关闭房间,则需要通知房间内的其他用户
          const user = Array.from(this.users).find(([userId, user]) => user.ws === ws);
          if (user) {
            const [userId, {ws, roomId, name}] = user;
            const room = this.rooms.get(roomId);
            if (room) {
              this.sendMessage(room.host, 'leave', {
                userId,
                username: name,
                message: `用户 ${name}(${userId}) 离开了房间`
              })
              room.clients = room.clients.filter(client => client !== ws)
            }
            this.users.delete(userId)
            return
          }

          const room = Array.from(this.rooms).find(([roomId, room]) => room.host === ws);
          if (room) {
            const [roomId, {name, clients}] = room;
            for (const client of clients) {
              this.sendMessage(client, 'close', {
                roomId,
                roomName: name,
                message: `房间 ${name}(${roomId}) 已关闭`
              })
            }
            this.rooms.delete(roomId)

            // 通知所有用户 有房间关闭
            for (const [userId, user] of this.users) {
              const {ws: userWs} = user;
              this.sendMessage(userWs, 'updateRooms', {
                roomId,
                type: 'close'
              })
            }
          }
        },
        message: (ws, message: string) => {
          const payload: {
            type: MessageKeys;
            data: Record<string, any>;
          } = JSON.parse(message);
          const {type, data} = payload;

          const handler = this.messageHandlers[type];
          handler?.(ws, data)
        }
      },
    })
    console.log(`bun server is running at http://localhost:${this.port}`)
  }
}

const share = new Share()
await share.start()

3. 实现主播页面

根目录下新建src文件夹;src文件夹下新建main.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        html, body {
            margin: 0;
            padding: 0;
            font-family: Arial, sans-serif;
            height: 100vh;
            width: 100vw;
            overflow: hidden;
        }

        .container {
            width: 100%;
            height: 100%;
            display: flex;
            flex-direction: column;
            overflow: hidden;
        }

        .header {
            display: flex;
            justify-content: space-between;
            padding: 10px;
            border-bottom: 1px solid #ccc;
            align-items: center;
        }

        .header h1 {
            margin: 0;
        }

        .header-main {
            flex: 1;
        }

        .header-main button {
            padding: 5px 10px;
            border: 1px solid #ccc;
            border-radius: 5px;
        }

        .main {
            margin-top: 10px;
            flex: 1;
            display: flex;
            overflow: hidden;
        }

        #preview {
            flex: 1;
            border: 1px solid #ccc;
            overflow: hidden;
        }

        .message {
            width: 300px;
            border: 1px solid #ccc;
            margin: 0 0 0 10px;
            padding: 0;
            display: flex;
            flex-direction: column;
            overflow: hidden;
        }

        .message-list {
            padding: 0 0 0 24px;
            margin: 0;
            flex: 1;
            overflow-y: auto;
        }

        #danmaku {
            display: flex;
            padding: 10px;
            border-top: 1px solid #ccc;
        }

        #danmaku input {
            flex: 1;
            padding: 5px;
            border: 1px solid #ccc;
            border-radius: 5px;
        }

        #danmaku button {
            padding: 5px 10px;
            border: 1px solid #ccc;
            border-radius: 5px;
            margin-left: 10px;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="header">
        <h1 id="title"></h1>
        <div class="header-main">
            <button id="button">开始共享</button>
            <button id="share-link">分享房间</button>
        </div>
    </div>
    <div class="main">
        <video id="preview"></video>
        <div class="message">
            <ul class="message-list"></ul>
            <form id="danmaku">
                <input type="text" placeholder="请输入消息">
                <button type="submit">发送</button>
            </form>
        </div>
    </div>
</div>
</body>
<script>
    class Main {
        constructor() {
            const params = new URLSearchParams(location.search)
            const url = new URL(location.href)
            this.preview = document.getElementById('preview')
            this.socketUrl = 'ws://localhost:3000/ws'
            this.title = document.getElementById('title')
            this.button = document.getElementById('button')
            this.shareButton = document.getElementById('share-link')
            this.messageContainer = document.querySelector('.message-list')
            this.socket = null
            this.roomId = params.get('id') || this.generateClientId()
            this.roomName = params.get('name') || ''
            this.isSharing = false
            this.stream = null
            this.peers = new Map
            this.cover = ''
            this.danmaku = document.getElementById('danmaku')
        }

        start() {
            this.inputRoomName()
            this.registerEvent()
        }

        messageHandler = {
            joined: async (data) => {
                console.log('join')
                const li = document.createElement('li')
                li.textContent = `欢迎 ${data.username}(${data.userId}) 加入房间`
                this.setMessage(li)

                // 给新加入的用户发送offer
                const peer = new RTCPeerConnection()
                this.stream.getTracks().forEach(track => peer.addTrack(track, this.stream))
                peer.onicecandidate = (e) => {
                    if (e.candidate) {
                        this.sendMessage('icecandidate', {
                            candidate: e.candidate,
                            userId: data.userId,
                            roomId: this.roomId
                        })
                    }
                }

                const offer = await peer.createOffer()
                await peer.setLocalDescription(offer)
                this.sendMessage('offer', {
                    offer,
                    userId: data.userId,
                    roomId: this.roomId
                })

                this.peers.set(data.userId, peer)
            },
            leave: (data) => {
                console.log('leave')
                const li = document.createElement('li')
                li.classList.add('leave')
                li.textContent = `${data.username}(${data.userId}) 离开房间`
                this.setMessage(li)
            },
            answer: async (data) => {
                console.log('answer')
                const {answer, userId} = data
                const peer = this.peers.get(userId)
                await peer.setRemoteDescription(answer)
            },
            icecandidate: (data) => {
                console.log('icecandidate')
                const {candidate, userId} = data
                const peer = this.peers.get(userId)
                peer.addIceCandidate(new RTCIceCandidate(candidate))
            },
            danmaku: (data) => {
                const li = document.createElement('li')
                const possessor = document.createElement('label')
                const {roomId, admin, message, username, userId} = data
                let part = ''
                if (admin) part = '我'
                else part = `${username}(${userId})`
                possessor.textContent = `${part}说:`
                const content = document.createElement('span')
                content.textContent = message
                li.appendChild(possessor)
                li.appendChild(content)
                this.setMessage(li)
            }
        }

        sendMessage(type, data) {
            this.socket.send(JSON.stringify({
                type,
                data
            }))
        }

        generateClientId() {
            return Math.random().toString().substring(2, 9)
        }

        inputRoomName() {
            while (!this.roomName) {
                this.roomName = prompt('请输入')
            }
            this.title.textContent = `hi, ${this.roomName}(${this.roomId})`
            const params = new URLSearchParams({
                id: this.roomId,
                name: this.roomName
            })
            history.pushState(null, '', `?${params}`)
        }

        registerEvent() {
            this.button.addEventListener('click', this.buttonClick.bind(this))
            this.danmaku.addEventListener('submit', this.danmakuSubmit.bind(this))
            this.shareButton.addEventListener('click', () => {
                const url = new URL(`${location.origin}/src/watch.html`)
                url.searchParams.set('id', this.roomId)
                url.searchParams.set('name', this.roomName)
                navigator.clipboard.writeText(url.href)
            })
        }

        async buttonClick() {
            this.isSharing ? await this.stopShare() : await this.startShare()
        }

        async stopShare() {
            if (!this.stream) return
            this.stream.getTracks().forEach(track => track.stop())
            this.stream = null
            await this.preview.pause()
            this.preview.srcObject = null
            this.socket?.close()
            this.socket = null
            this.peers.forEach(peer => peer.close())
            this.peers.clear()

            const li = document.createElement('li')
            li.textContent = `房间 ${this.roomName}(${this.roomId}) 已关闭`
            this.setMessage(li)

            this.isSharing = false
            this.button.textContent = '开始共享'
        }

        async startShare() {
            this.stream = await navigator.mediaDevices.getDisplayMedia({
                video: true,
                audio: false
            })

            this.stream.getTracks().forEach(track => {
                track.onended = this.stopShare.bind(this)
            })

            this.preview.srcObject = this.stream
            await this.preview.play()

            // 获取第一帧作为封面
            const canvas = document.createElement('canvas')
            const ctx = canvas.getContext('2d')
            canvas.width = this.preview.videoWidth
            canvas.height = this.preview.videoHeight
            ctx.drawImage(this.preview, 0, 0, canvas.width, canvas.height)
            this.cover = canvas.toDataURL('image/png')

            this.socket = new WebSocket(this.socketUrl)
            this.socket.onopen = this.socketOnOpen.bind(this)
            this.socket.onmessage = this.socketOnMessage.bind(this)
            this.socket.onerror = this.socketOnError.bind(this)
            this.socket.onclose = this.socketOnClose.bind(this)

            const li = document.createElement('li')
            li.textContent = `房间 ${this.roomName}(${this.roomId}) 已创建`
            this.setMessage(li)

            this.isSharing = true
            this.button.textContent = '停止共享'
        }

        socketOnOpen() {
            this.sendMessage('create', {
                roomId: this.roomId,
                roomName: this.roomName,
                cover: this.cover
            })
        }

        socketOnMessage(e) {
            const payload = JSON.parse(e.data)
            const {data, type} = payload
            this.messageHandler[type](data)
        }

        socketOnError() {
        }

        socketOnClose() {
            this.stopShare()
        }

        setMessage(li) {
            this.messageContainer.appendChild(li)
            this.messageContainer.scrollTop = this.messageContainer.scrollHeight
        }

        danmakuSubmit(e) {
            e.preventDefault()
            if (!this.socket) return
            const input = this.danmaku.querySelector('input')
            const message = input.value
            if (!message) return
            this.sendMessage('danmaku', {
                message,
                roomId: this.roomId,
                admin: true
            })
            input.value = ''
        }
    }

    const main = new Main()
    main.start()
</script>
</html>

4. 实现观众页面

/src/watch.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        html, body {
            margin: 0;
            padding: 0;
            font-family: Arial, sans-serif;
            height: 100vh;
            width: 100vw;
            overflow: hidden;
        }
        .container {
            width: 100%;
            height: 100%;
            display: flex;
            flex-direction: column;
            overflow: hidden;
        }

        .header {
            display: flex;
            justify-content: space-between;
            padding: 10px;
            border-bottom: 1px solid #ccc;
            align-items: center;
        }

        .header h1 {
            margin: 0;
        }

        .main {
            margin-top: 10px;
            flex: 1;
            display: flex;
            overflow: hidden;
        }

        #preview {
            flex: 1;
            border: 1px solid #ccc;
            overflow: hidden;
        }

        .message {
            width: 300px;
            border: 1px solid #ccc;
            margin: 0 0 0 10px;
            padding: 0;
            display: flex;
            flex-direction: column;
            overflow: hidden;
        }

        .message-list {
            padding: 0 0 0 24px;
            margin: 0;
            flex: 1;
            overflow-y: auto;
        }

        #danmaku {
            display: flex;
            padding: 10px;
            border-top: 1px solid #ccc;
        }

        #danmaku input {
            flex: 1;
            padding: 5px;
            border: 1px solid #ccc;
            border-radius: 5px;
        }

        #danmaku button {
            padding: 5px 10px;
            border: 1px solid #ccc;
            border-radius: 5px;
            margin-left: 10px;
        }

        .room-list {
            padding: 0;
            list-style: none;
            width: 300px;
            border: 1px solid #ccc;
            margin: 0 10px 0 0;
        }

        .room-list li {
            padding: 10px;
            border-bottom: 1px solid #ccc;
        }

        .room-list li img {
            width: 100%;
            display: block;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="header">
        <h1 id="title"></h1>
    </div>
    <div class="main">
        <ul class="room-list"></ul>
        <video id="preview"></video>
        <div class="message">
            <ul class="message-list"></ul>
            <form id="danmaku">
                <input type="text" placeholder="请输入消息">
                <button type="submit">发送</button>
            </form>
        </div>
    </div>
</div>
<script>
    class Watch {
        constructor() {
            const params = new URLSearchParams(location.search)
            const url = new URL(location.href)
            this.video = document.getElementById('preview')
            this.title = document.getElementById('title')
            this.messageContainer = document.querySelector('.message-list')
            this.socket = null
            this.socketUrl = `ws://${url.host}/ws`
            this.roomContainer = document.querySelector('.room-list')
            this.peer = null
            this.username = params.get('uname')
            this.userId = params.get('uid') || this.generateClientId()
            this.danmaku = document.getElementById('danmaku')
        }

        get roomId() {
            const params = new URLSearchParams(location.search)
            return params.get('id')
        }

        set roomId(value) {
            const params = new URLSearchParams(location.search)
            params.set('id', value)
            history.pushState(null, '', `?${params}`)
        }

        messageHandler = {
            offer: async (data) => {
                const {offer} = data
                await this.peer.setRemoteDescription(offer)
                const answer = await this.peer.createAnswer()
                await this.peer.setLocalDescription(answer)
                this.sendMessage('answer', {
                    answer,
                    userId: this.userId,
                    roomId: this.roomId
                })
            },
            answer: (data) => {
                console.log(data)
            },
            error: (e) => {
                const li = document.createElement('li')
                li.textContent = e.message
                this.setMessage(li)
            },
            success: (e) => {
                const li = document.createElement('li')
                li.textContent = e.message
                this.setMessage(li)
            },
            close: (e) => {
                const li = document.createElement('li')
                li.textContent = e.message
                this.setMessage(li)

                this.video.srcObject?.getTracks().forEach(track => track.stop())
                this.peer.close()
            },
            icecandidate: async (data) => {
                await this.peer.addIceCandidate(data.candidate)
            },
            updateRooms: (data) => {
                this.getRoom()
                const {type, roomId} = data
                if (type === 'create' && roomId === this.roomId) {
                    this.enterRoom()
                }
            },
            danmaku: (data) => {
                const li = document.createElement('li')
                const possessor = document.createElement('label')
                const {roomId, admin, message, username, userId} = data
                let part = ''
                if (admin) part = 'UP主'
                else if (userId === this.userId) part = '我'
                else part = `${username}(${userId})`
                possessor.textContent = `${part}说:`
                const content = document.createElement('span')
                content.textContent = message
                li.appendChild(possessor)
                li.appendChild(content)
                this.setMessage(li)
            }
        }

        async start() {
            this.inputUsername()
            this.registerEvent()
            await this.getRoom()
            await this.enterRoom()
        }

        setMessage(li) {
            this.messageContainer.appendChild(li)
            this.messageContainer.scrollTop = this.messageContainer.scrollHeight
        }
        generateClientId() {
            return Math.random().toString().substring(2, 9)
        }

        inputUsername() {
            while (!this.username) {
                this.username = prompt('请输入用户名')
            }
            this.title.textContent = `hi, ${this.username}(${this.userId})`
            const params = new URLSearchParams(location.search)
            params.set('uid', this.userId)
            params.set('uname', this.username)
            history.pushState(null, '', `?${params}`)
        }

        async getRoom() {
            const res = await fetch('/api/rooms')
            const rooms = await res.json()
            this.roomContainer.innerHTML = ''
            const fragment = document.createDocumentFragment()
            for (const room of rooms) {
                const li = document.createElement('li')
                const a = document.createElement('a')
                const params = new URLSearchParams({
                    id: room.id,
                    uid: this.userId,
                    uname: this.username
                })

                const cover = document.createElement('img')
                cover.src = room.cover
                a.href = `?${params}`
                a.textContent = `${room.name}(${room.id})的房间`
                li.appendChild(cover)
                li.appendChild(a)
                fragment.appendChild(li)
            }
            this.roomContainer.appendChild(fragment)
        }

        async enterRoom() {
            // if (!this.roomId) return
            this.socket = new WebSocket(this.socketUrl)
            this.peer = new RTCPeerConnection()
            this.peer.ontrack = (e) => {
                this.video.srcObject = e.streams[0]
                this.video.play().catch(this.play.bind(this))
            }

            this.peer.onicecandidate = (e) => {
                if (e.candidate) {
                    this.sendMessage('icecandidate', {
                        candidate: e.candidate,
                        userId: this.userId,
                        roomId: this.roomId
                    })
                }
            }
            this.socket.onopen = this.socketOnOpen.bind(this)
            this.socket.onmessage = this.socketOnMessage.bind(this)
            this.socket.onerror = this.socketOnError.bind(this)
        }

        // 手动点击播放
        play() {
            const li = document.createElement('li')
            li.style.color = 'red'
            const span = document.createElement('span')
            span.textContent = '由于浏览器自动播放策略,'
            const a = document.createElement('a')
            a.href = 'javascript:void(0)'
            a.textContent = '点击这里播放'
            a.onclick = () => {
                this.video.play()
                li.remove()
            }
            li.appendChild(span)
            li.appendChild(a)
            this.setMessage(li)
        }

        socketOnOpen() {
            this.sendMessage('join', {
                roomId: this.roomId,
                userId: this.userId,
                username: this.username
            })
        }

        socketOnMessage(e) {
            const payload = JSON.parse(e.data)
            const {data, type} = payload
            this.messageHandler[type](data)
        }

        sendMessage(type, data) {
            this.socket.send(JSON.stringify({
                type,
                data
            }))
        }

        socketOnError() {
            this.video.srcObject?.getTracks().forEach(track => track.stop())
            this.peer.close()
        }

        registerEvent() {
            this.danmaku.addEventListener('submit', this.danmakuSubmit.bind(this))
        }

        danmakuSubmit(e) {
            e.preventDefault()
            if (!this.socket) return
            const input = this.danmaku.querySelector('input')
            const message = input.value
            if (!message) return
            this.sendMessage('danmaku', {
                message,
                roomId: this.roomId,
                username: this.username,
                userId: this.userId
            })
            input.value = ''
        }
    }

    const watch = new Watch()
    watch.start()
</script>
</body>
</html>

5. 入口

/src/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        a {
            display: block;
            width: 100%;
            height: 100px;
            line-height: 100px;
            text-align: center;
            border: 1px solid #ccc;
            border-radius: 4px;
            font-size: 24px;
            font-family: Arial, sans-serif;
            text-decoration: none;
        }

        a + a {
            margin-top: 10px;
        }
    </style>
</head>
<body>
<div>
    <a href="/src/main.html">我要分享</a>
    <a href="/src/watch.html">我要观看</a>
</div>
</body>
</html>

6. 运行

bun run ./index.ts

7. 效果

image

标签:const,data,userId,li,BUN,bun,roomId,WebRTC,ws
From: https://www.cnblogs.com/coderDemo/p/18597848

相关文章

  • Jitsi 的 STUN 服务器在 WebRTC 中的应用
    WebRTC允许浏览器进行实时音视频通话,无需额外的插件或安装软件。在WebRTC的实现过程中,STUN和TURN协议扮演着至关重要的角色。它们负责解决NAT穿透的问题,确保客户端之间能够建立起可靠的点对点(P2P)连接。在Jitsi框架中,STUN服务器是NAT穿透的关键组成部分,帮助WebRTC实......
  • 深入了解 Jitsi 的 TURN 服务器及其在 WebRTC 中的应用
    在实时音视频通信中,WebRTC是一项核心技术,能够实现点对点(P2P)的直接通信。然而,在某些复杂网络环境(如对称NAT或企业防火墙)中,直接通信可能会失败。为了确保通信的稳定性,TURN服务器提供了一种解决方案,通过中继数据流实现客户端之间的连接。1.什么是TURN服务器?TURN是一种网......
  • 在 Debian 中使用 APT 包管理工具通过 Ubuntu PPA 安装最新软件包
    在Debian中使用APT包管理工具通过UbuntuPPA安装最新软件包Neovim0.10.0在24年5月发布了.考虑许久后笔者决定试着从Vim9转向Neovim.其中遇到的第一个问题是,Debian的默认源里只有Neovim0.7.7,因此寻找下载最新软件包的方法,并做下详细记录.虽然本文初衷......
  • Docker Ubuntu 上安装 ssh 和连接 ssh
    Docker安装Ubuntu首先从云上拉取ubuntu的镜像dockerpullubuntu使用dockerimages或dockerimagels查看刚才摘取下来的镜像如上图示镜像ID为b1d9df8ab815启动镜像输入命令dockerrun-itd-p6789:22b1d9df8ab815,表示在后台启动镜像,并将本机的6789......
  • Ubuntu + Caddy 搭建简易文件下载站
    安装sudoaptinstall-ydebian-keyringdebian-archive-keyringapt-transport-httpscurlcurl-1sLf'https://dl.cloudsmith.io/public/caddy/stable/gpg.key'|sudogpg--dearmor-o/usr/share/keyrings/caddy-stable-archive-keyring.gpgcurl-1sLf'......
  • 在Ubuntu中配置环境变量
    最近在使用jaka机器人时候,每次打开终端都需要导入环境Linux需要将libjakaAPI.so和jkrc.so放在同一个文件夹下,并添加当前文件夹路径到环境变量, (yolov8)rebot@wp:~/yolov8-yolo/ultralytics-main$/home/rebot/anaconda3/envs/yolov8/bin/python/home/rebot/yolov8-y......
  • ubuntu24.04在线安装Docker,设置容器目录与基础配置
    1、设置apt#AddDocker'saliyunGPGkey:sudoaptupdate#安装必要的软件包sudoaptinstall-yapt-transport-httpsca-certificatescurlsoftware-properties-common#添加阿里云GPT密钥管理sudocurl-fsSLhttps://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg......
  • ubuntu24.04挂载2.2T以上硬盘,实现开启挂载
    1、su进入root用户,输入密码。2、查看现有硬盘使用情况,可以发现数据硬盘暂未挂载到目录。df-hFilesystemSizeUsedAvailUse%Mountedonudev63G063G0%/devtmpfs13G1.2M13G1%/run/dev/vda140G5.3G32G......
  • 在Ubuntu 22.04上搭建Kubernetes集群
    Kubernetes简介什么是Kubernetes?Kubernetes(常简称为K8s)是一个强大的开源平台,用于管理容器化应用程序的部署、扩展和运行。它最初由Google设计并捐赠给CloudNativeComputingFoundation(CNCF)来维护,现在已经成为容器编排领域的事实标准。Kubernetes的核心特性服务......
  • EasyRTC的WebRTC点对点p2p双向视频通话微信小程序浏览器p2p视频对讲技术方案
    技术参数显示方式:浏览器内核传输方式:P2P支持浏览器类型:Chrome、Edge、Safari音频:双向对讲视频时延:200ms技术详情WebRTC应用IPC支持web无插件可视对讲支持嵌入式设备系统:Linux、ARM、LiteOS、RTOS、Android、iOSP2P传输可节省服务器流量费用可用HTML5架构web功能应用,多通......