init commit

This commit is contained in:
Fang_Zhijian 2024-05-08 18:01:01 +08:00
commit 1eed1428f2
20 changed files with 4632 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

32
README.md Normal file
View File

@ -0,0 +1,32 @@
# BLive Coyote
郊狼B站直播玩法送礼物控制郊狼主机输出
## 项目简介
> [!TIP]
> 本项目仍在开发中,目前可体验基础功能。
>
> 本项目为个人学习项目,如有错误敬请批评指正,欢迎 PR。
本项目以 [Bilibili 直播&互玩 JavaScript Demo](https://open-live.bilibili.com/document/a7bd5377-ad7d-a273-25ae-28caf37a7a85) 和 [DG-LAB SOCKET控制-控制端开源](https://github.com/DG-LAB-OPENSOURCE/DG-LAB-OPENSOURCE/tree/main/socket) 为基础,实现了直播间送礼物控制郊狼主机输出的功能。
- 郊狼 WebSocket 后端请参考:[DG-LAB-OPENSOURCE](https://github.com/DG-LAB-OPENSOURCE/DG-LAB-OPENSOURCE/tree/main/socket/BackEnd(Node))
- 哔哩哔哩直播互动玩法服务端请参考:[JavaScript Demo](https://open-live.bilibili.com/document/a7bd5377-ad7d-a273-25ae-28caf37a7a85)
## 目录结构
```
Client
├─public
│ └─css
└─src
├─assets
├─socket
└─types
```
## 项目启动
```bash
cd Client
npm install
npm run dev
```
主播身份码及 app_id 获取请参考 [开放平台-直播&互玩接入文档:常见问题](https://open-live.bilibili.com/document/5dffc297-6fd2-41ff-bd45-6e8b89e2a68e)

17
index.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/css/notyf.min.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

3283
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "poros-demo-client",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite --port 8888 --host 0.0.0.0 ",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.2.25"
},
"devDependencies": {
"@originjs/vite-plugin-commonjs": "1.0.3",
"@types/node": "17.0.25",
"@vitejs/plugin-vue": "2.3.0",
"axios": "0.26.1",
"typescript": "4.5.4",
"vite": "2.9.0",
"vue-tsc": "0.29.8",
"ws": "8.17.0",
"qrcode": "1.5.3",
"notyf": "3.0.0"
}
}

1
public/css/notyf.min.css vendored Normal file

File diff suppressed because one or more lines are too long

611
public/css/style.css Normal file
View File

@ -0,0 +1,611 @@
/* Copyright 2013 The Chromium Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file. */
html,
body {
padding: 0;
margin: 0;
width: 100%;
background-color: #171717;
}
.icon {
-webkit-user-select: none;
user-select: none;
display: inline-block;
}
.icon-offline {
position: relative;
}
.hidden {
display: none;
}
button {
cursor: pointer;
background-color: #fce9a7;
color: #000;
padding: 2px 10px;
border-radius: 3px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
-ms-border-radius: 3px;
-o-border-radius: 3px;
}
button:hover {
background-color: #ffe668;
}
select {
background-color: #fce9a7;
border-radius: 3px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
-ms-border-radius: 3px;
-o-border-radius: 3px;
}
/* Offline page */
.offline .interstitial-wrapper {
color: #2b2b2b;
font-size: 1em;
line-height: 1.55;
margin: 0 auto;
max-width: 600px;
padding-top: 200px;
width: 100%;
}
.offline .runner-container {
height: 150px;
max-width: 600px;
overflow: hidden;
position: absolute;
top: 180px;
width: 44px;
transition: top 0.1s ease-in-out;
-webkit-transition: top 0.1s ease-in-out;
-moz-transition: top 0.1s ease-in-out;
-ms-transition: top 0.1s ease-in-out;
-o-transition: top 0.1s ease-in-out;
}
.offline .runner-canvas {
height: 150px;
max-width: 600px;
opacity: 1;
overflow: hidden;
position: absolute;
top: 0;
z-index: 2;
}
.offline .controller {
background: rgba(247, 247, 247, .1);
height: 100vh;
left: 0;
position: absolute;
top: 0;
width: 100vw;
z-index: 1;
}
#offline-resources {
display: none;
}
@media (max-width: 420px) {
.suggested-left>#control-buttons,
.suggested-right>#control-buttons {
float: none;
}
.snackbar {
left: 0;
bottom: 0;
width: 100%;
border-radius: 0;
}
}
@media (max-height: 350px) {
h1 {
margin: 0 0 15px;
}
.icon-offline {
margin: 0 0 10px;
}
.interstitial-wrapper {
margin-top: 5%;
}
.nav-wrapper {
margin-top: 30px;
}
}
@media (min-width: 600px) and (max-width: 736px) and (orientation: landscape) {
.offline .interstitial-wrapper {
margin-left: 0;
margin-right: 0;
}
}
@media (min-width: 420px) and (max-width: 736px) and (min-height: 240px) and (max-height: 420px) and (orientation:landscape) {
.interstitial-wrapper {
margin-bottom: 100px;
}
}
@media (min-height: 240px) and (orientation: landscape) {
.offline .interstitial-wrapper {
margin-bottom: 90px;
}
.icon-offline {
margin-bottom: 20px;
}
}
@media (max-height: 320px) and (orientation: landscape) {
.icon-offline {
margin-bottom: 0;
}
.offline .runner-container {
top: 10px;
}
}
@media (max-width: 240px) {
.interstitial-wrapper {
overflow: inherit;
padding: 0 8px;
}
}
.arcade-mode,
.arcade-mode .runner-container,
.arcade-mode .runner-canvas {
image-rendering: pixelated;
max-width: 100%;
overflow: hidden;
}
.arcade-mode #buttons,
.arcade-mode #main-content {
opacity: 0;
overflow: hidden;
}
.arcade-mode .interstitial-wrapper {
height: 100vh;
max-width: 100%;
overflow: hidden;
}
.arcade-mode .runner-container {
left: 0;
margin: auto;
right: 0;
transform-origin: top center;
transition: transform 250ms cubic-bezier(0.4, 0, 1, 1) 400ms;
z-index: 2;
}
#qrcode-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.85);
z-index: 100;
display: flex;
justify-content: center;
align-items: center;
z-index: 1002;
}
#qrcode-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 1002;
}
#qrcode {
position: relative;
z-index: 1002;
border: 3px solid #fff;
}
#qrcode-text {
font-size: 16px;
text-align: center;
margin-bottom: 30px;
}
#qrcode-text p {
white-space: pre-wrap;
/* 自动换行 */
}
.close-qrcode {
color: #000000;
cursor: pointer;
background-color: #ffe99d;
padding: 5px 30px;
border-radius: 3px;
margin-top: 40px;
}
.header-container {
position: fixed;
display: flex;
width: 100%;
height: 18%;
padding: 20px;
box-sizing: border-box;
min-height: 140px;
min-width: 500px;
z-index: 999;
}
.btn {
width: 145px;
padding: 10px;
background-color: #ffe99d;
border-radius: 5px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
-ms-border-radius: 5px;
-o-border-radius: 5px;
cursor: pointer;
}
.status-container {
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: column;
margin-left: 20px;
padding: 12px 20px;
width: 15%;
justify-content: center;
align-items: center;
border: 1px solid #ffe99d;
border-radius: 5px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
-ms-border-radius: 5px;
-o-border-radius: 5px;
right: 0;
color: #ffe99d;
}
.red {
color: rgb(255, 67, 67);
font-weight: bold;
}
.red-background {
background-color: rgb(255, 70, 70);
}
.connect-btn {
width: 100%;
border: none;
display: flex;
justify-content: space-between;
}
.connect-btn button:first-child {
margin-right: 10px;
}
.connect-btn button {
padding: 5px 10px;
margin-top: 15px;
width: 100%;
border-radius: 3px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
-ms-border-radius: 3px;
-o-border-radius: 3px;
}
.dg-controller {
width: 85%;
z-index: 1000;
color: #ffe99d;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
border: 1px solid #ffe99d;
border-radius: 5px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
-ms-border-radius: 5px;
-o-border-radius: 5px;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
}
/* 1020px */
@media screen and (max-width: 1020px) {
.dg-controller {
width: 80%;
}
.status-container {
width: 20%;
}
}
@media screen and (max-width: 792px) {
.dg-controller {
width: 72%;
}
.status-container {
width: 28%;
}
}
@media screen and (max-width: 600px) {
.dg-controller {
width: 65%;
}
.status-container {
width: 35%;
}
}
.dg-controller {
scrollbar-width: thin;
/* Firefox */
scrollbar-color: #ffe99d transparent;
/* Firefox */
}
.dg-controller::-webkit-scrollbar {
width: 8px;
/* Chrome, Safari */
}
.dg-controller::-webkit-scrollbar-track {
background-color: transparent;
/* Chrome, Safari */
}
.dg-controller::-webkit-scrollbar-thumb {
background-color: #ffe99d;
/* Chrome, Safari */
border-radius: 5px;
}
/* Firefox */
.dg-controller {
scrollbar-width: thin;
scrollbar-color: #ffe99d transparent;
}
.btn-container {
padding: 10px 20px;
display: flex;
align-items: center;
width: 85%;
}
.inputTime {
margin-top: 10px;
}
.inputTime input,
.btn-container input {
width: 50px;
}
.btn-container>* {
margin-right: 10px;
}
#custom-msg {
margin-top: 10px;
min-width: 200px;
min-height: 80px;
}
.intro-game {
color: #ffe99d;
width: 100%;
position: fixed;
left: 20px;
bottom: 10px;
}
.game-title {
display: flex;
}
.game-tips {
font-size: 20px;
font-weight: bold;
}
.tips-hide {
font-weight: bold;
margin-left: 15px;
border-radius: 3px;
padding: 2px 12px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
-ms-border-radius: 3px;
-o-border-radius: 3px;
}
.toggle-container {
display: inline-block;
position: relative;
width: 46px;
height: 22px;
background-color: #3b3b3b;
border-radius: 13px;
cursor: pointer;
-webkit-flex-shrink: 0;
/* Safari 和 Chrome */
-ms-flex-negative: 0;
/* IE 10+ */
flex-shrink: 0;
-moz-box-flex: 0;
/* Firefox */
}
.toggle-switch {
position: absolute;
cursor: pointer;
width: 16px;
height: 16px;
left: 5px;
bottom: 3px;
background-color: #ddd;
border-radius: 50%;
transition: .4s;
-webkit-transition: .4s;
-moz-transition: .4s;
-ms-transition: .4s;
-o-transition: .4s;
}
.toggle-container.on {
background-color: #fce9a7;
}
.toggle-container.on .toggle-switch {
transform: translateX(20px);
-webkit-transform: translateX(20px);
-moz-transform: translateX(20px);
-ms-transform: translateX(20px);
-o-transform: translateX(20px);
background-color: #000;
}
.question-img {
height: 20px;
}
.tooltip {
position: absolute;
background-color: #333;
color: #ffe99d;
padding: 5px;
border-radius: 4px;
display: none;
/* 默认隐藏 */
z-index: 1000;
}
select {
height: 22px;
}
.information {
display: flex;
flex-direction: column;
position: fixed;
top: 50%;
left: 50%;
padding: 10px 30px 30px 30px;
color: #ffe99d;
background-color: rgba(0, 0, 0);
border: 1px solid #ffe99d;
border-radius: 10px;
transform: translate(-50%, -50%);
-webkit-transform: translate(-50%, -50%);
-moz-transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
-o-transform: translate(-50%, -50%);
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
-ms-border-radius: 10px;
-o-border-radius: 10px;
z-index: 1003;
}
.information a {
color: inherit;
}
.information a:hover {
color: #fff;
}
.information-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.85);
z-index: 100;
display: flex;
justify-content: center;
align-items: center;
visibility: hidden;
z-index: 1002;
}
.information {
color: #dddddd;
}
.information h2 {
color: #ffe99d;
}
.information .notify{
color: #ffe99d;
}
.info-close {
display: flex;
justify-content: center;
align-items: center;
margin-top: 30px;
}
.info-close-btn {
font-size: 16px;
font-weight: bold;
background-color: #000;
border: 1px solid #ffe99d;
color: #ffe99d;
cursor: pointer;
padding: 10px 30px;
border-radius: 5px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
-ms-border-radius: 5px;
-o-border-radius: 5px;
}
.info-close-btn:hover {
background-color: #ffe99d;
color: #000;
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

229
src/App.vue Normal file
View File

@ -0,0 +1,229 @@
<script setup lang="ts">
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
import { ref } from "vue"
import axios from "axios"
import { createSocket, destroySocket } from "./socket/index"
import { createCoyoteSocket, closeCoyoteSocket, qrcodeSrc, qrcodeShow } from "./socket/coyote"
import { Notyf } from 'notyf'
const notyf = new Notyf({ duration: 3000 })
// API
const api = axios.create({
baseURL: "http://localhost:3000",
})
//
const codeId = ref("")
// app []
const appId = ref("")
// [ node server]
const gameId = ref("")
// v2server response websocket
const authBody = ref("")
const wssLinks = ref([])
// heartBeat Timer
const heartBeatTimer = ref<NodeJS.Timer>()
// be ready
clearInterval(heartBeatTimer.value!)
/**
* 测试请求鉴权接口
*/
const getAuth = () => {
api.post("/getAuth", {})
.then(({ data }) => {
console.log("-----鉴权成功-----")
notyf.success({ message: "鉴权成功" })
})
.catch((err) => {
console.log("-----鉴权失败-----")
notyf.error({ message: "鉴权失败" })
})
}
const heartBeatThis = (game_id) => {
//
api.post("/gameHeartBeat", {
game_id,
})
.then(({ data }) => {
console.log("-----心跳成功-----")
console.log("返回:", data)
})
.catch((err) => {
console.log("-----心跳失败-----")
})
}
/**
* @comment 注意所有的接口基于鉴权成功后才能正确返回
* 测试请求游戏开启接口
*/
const gameStart = () => {
api.post("/gameStart", {
code: codeId.value,
app_id: Number(appId.value),
})
.then(({ data }) => {
if (data.code === 0) {
const res = data.data
const { game_info, websocket_info } = res
const { auth_body, wss_link } = websocket_info
authBody.value = auth_body
wssLinks.value = wss_link
console.log("-----游戏开始成功-----")
console.log("返回GameId", game_info)
notyf.success({ message: "游戏开始成功" })
gameId.value = game_info.game_id
// v220s60s
heartBeatTimer.value = setInterval(() => {
heartBeatThis(game_info.game_id)
}, 20000)
handleCreateSocket()
} else {
console.log("-----游戏开始失败-----")
console.log("原因:", data)
notyf.error({ message: "游戏开始失败" })
}
})
.catch((err) => {
console.log("-----游戏开始失败-----")
console.log(err)
notyf.error({ message: "游戏开始失败" })
})
}
/**
* @comment 基于gameStart成功后才会关闭正常否则获取不到game_id
* 测试请求游戏关闭接口
*/
const gameEnd = () => {
api.post("/gameEnd", {
game_id: gameId.value,
app_id: Number(appId.value),
})
.then(({ data }) => {
if (data.code === 0) {
console.log("-----游戏关闭成功-----")
console.log("返回:", data)
//
authBody.value = ""
wssLinks.value = []
clearInterval(heartBeatTimer.value!) //
handleDestroySocket()
console.log("-----心跳关闭成功-----")
notyf.success({ message: "游戏关闭成功" })
} else {
console.log("-----游戏关闭失败-----")
console.log("原因:", data)
notyf.error({ message: "游戏关闭失败" })
}
})
.catch((err) => {
console.log("-----游戏关闭失败-----")
console.log(err)
notyf.error({ message: "游戏关闭失败" })
})
}
/**
* 测试创建长长连接接口
*/
const handleCreateSocket = () => {
if (authBody.value && wssLinks.value) {
createSocket(authBody.value, wssLinks.value)
console.log("-----长连接创建成功-----")
}
}
/**
* 测试销毁长长连接接口
*/
const handleDestroySocket = () => {
destroySocket()
console.log("-----长连接销毁成功-----")
}
/**
* 测试按钮
*/
const test = () => {
let map = {
"123": "AAWWWWW",
"123456": "aaaaaaaa",
}
notyf.success({ message: '测试吐司', duration: 3000, ripple: true })
notyf.success({ message: map[123], duration: 3000, ripple: true })
}
/**
* 显示二维码
*/
const showqrcode = () => {
qrcodeShow.value = true;
}
/**
* 隐藏二维码
*/
const hideqrcode = () => {
qrcodeShow.value = false;
}
</script>
<template>
<div id="qrcode-overlay" v-show="qrcodeShow">
<div id="qrcode-container">
<div id="qrcode-text">
<p>使用DG-LAB APP扫码建立WebSocket链接</p>
<p>请先开始游戏与直播间建立连接</p>
</div>
<div id="qrcode-img">
<img id="qrcode" :src="qrcodeSrc">
</div>
<div class="close-qrcode" @click="hideqrcode">关闭</div>
</div>
</div>
<div>
<h2>游戏设置</h2>
<div class="form">
<label>主播身份码</label>
<input type="password" placeholder="填写主播身份码" v-model="codeId"/>
<label>app_id</label>
<input type="text" placeholder="填写 app_id" v-model="appId" />
<button @click="gameStart">游戏开始</button>
<button @click="gameEnd">游戏结束</button>
<button @click="createCoyoteSocket">连接郊狼</button>
<button @click="closeCoyoteSocket">断开郊狼</button>
</div>
<hr />
<h2>调试选项</h2>
<div class="form">
<button @click="getAuth">鉴权</button>
<button @click="test">测试</button>
</div>
</div>
</template>
<style>
#app {
color: #fff;
margin: 60px 50px;
}
.form {
display: flex;
flex-direction: column;
}
.form input,
.form button {
width: 300px;
height: 50px;
margin: 10px 0;
font-size: 18px;
}
</style>

8
src/assets/danmaku-websocket.min.js vendored Normal file

File diff suppressed because one or more lines are too long

19
src/assets/dataMap.ts Normal file
View File

@ -0,0 +1,19 @@
// Desc: 用于存放数据映射表
// 礼物对应的波形
const waveData = {
"31037": `["0A0A0A0A00000000","0A0A0A0A0A0A0A0A","0A0A0A0A14141414","0A0A0A0A1E1E1E1E","0A0A0A0A28282828","0A0A0A0A32323232","0A0A0A0A3C3C3C3C","0A0A0A0A46464646","0A0A0A0A50505050","0A0A0A0A5A5A5A5A","0A0A0A0A64646464"]`,
"31164": `["0A0A0A0A00000000","0D0D0D0D0F0F0F0F","101010101E1E1E1E","1313131332323232","1616161641414141","1A1A1A1A50505050","1D1D1D1D64646464","202020205A5A5A5A","2323232350505050","262626264B4B4B4B","2A2A2A2A41414141"]`,
"32609": `["4A4A4A4A64646464","4545454564646464","4040404064646464","3B3B3B3B64646464","3636363664646464","3232323264646464","2D2D2D2D64646464","2828282864646464","2323232364646464","1E1E1E1E64646464","1A1A1A1A64646464"]`
}
// 礼物id和礼物名的对应关系
const giftData = {
"31036": "小花花",
"31039": "牛哇牛哇",
"32609": "棒棒糖",
"31037": "打call",
"31164": "粉丝团灯牌",
}
export { waveData }

1
src/assets/notyf.min.css vendored Normal file

File diff suppressed because one or more lines are too long

9
src/env.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}

5
src/main.ts Normal file
View File

@ -0,0 +1,5 @@
import { createApp } from "vue"
import App from "./App.vue"
createApp(App).mount("#app")

171
src/socket/coyote.ts Normal file
View File

@ -0,0 +1,171 @@
import {ref} from "vue";
import { Notyf } from 'notyf'
import { waveData } from "../assets/dataMap";
const notyf = new Notyf({ duration: 4000 })
const QRCode = require('qrcode')
let channelAStrength = 0; // A通道强度
let channelBStrength = 0; // B通道强度
let connectionId = ""; // 从接口获取的连接标识符
let targetWSId = ""; // 发送目标
let fangdou = 500; //500毫秒防抖
let fangdouSetTimeOut; // 防抖定时器
let followAStrength = false; //跟随AB软上限
let followBStrength = false;
let wsConn; // 全局ws链接
const feedBackMsg = {
"feedback-0": "A通道○",
"feedback-1": "A通道△",
"feedback-2": "A通道□",
"feedback-3": "A通道☆",
"feedback-4": "A通道⬡",
"feedback-5": "B通道○",
"feedback-6": "B通道△",
"feedback-7": "B通道□",
"feedback-8": "B通道☆",
"feedback-9": "B通道⬡",
}
const qrcodeSrc = ref("")
const qrcodeShow = ref(false)
QRCode.toDataURL("https://www.dungeon-lab.com/app-download.php#DGLAB-SOCKET#ws://39.108.168.199:9999/", function (err, url) {
//console.log(url)
qrcodeSrc.value = url
})
function createCoyoteSocket() {
wsConn = new WebSocket('ws://coyote.babyfang.cn:9999/');
wsConn.onopen = function (event) {
console.log("WebSocket连接已建立");
}
wsConn.onmessage = function (event) {
let msg = {} as any
try {
msg = JSON.parse(event.data);
} catch (e) {
console.log(event.data);
return;
}
console.log(event.data)
switch (msg.type) {
case 'bind':
if (!msg.targetId) {
connectionId = msg.clientId;
console.log(`收到clientId${connectionId}`);
QRCode.toDataURL("https://www.dungeon-lab.com/app-download.php#DGLAB-SOCKET#ws://coyote.babyfang.cn:9999/" + connectionId, function (err, url) {
//console.log(url)
qrcodeSrc.value = url
})
qrcodeShow.value = true
} else {
if (msg.clientId != connectionId) {
console.log("错误的clientId")
return;
}
targetWSId = msg.targetId;
console.log("收到targetId: " + msg.targetId + ", msg: " + msg.message);
qrcodeShow.value = false
notyf.success({message: "郊狼连接成功"})
}
break;
case 'break':
if (msg.targetId != targetWSId)
return;
console.log("收到断开连接指令")
notyf.error({ message: "收到断开连接指令" })
//location.reload();
break;
case 'error':
if (msg.targetId != targetWSId)
return;
console.log("对方已断开code" + msg.message)
notyf.error({ message: "对方已断开(" + msg.message + "" })
break;
case 'msg':
const result: { type: string; numbers: number[] }[] = []
if(msg.message.includes("strength")) {
const numbers = msg.message.match(/\d+/g).map(Number)
result.push({ type: "strength", numbers: numbers })
}
}
}
}
function sendWsMsg(messageObj) {
messageObj.clientId = connectionId;
messageObj.targetId = targetWSId;
if (!messageObj.hasOwnProperty('type'))
messageObj.type = "msg";
wsConn.send(JSON.stringify((messageObj)));
}
function addOrIncrease(type, channelIndex, strength) {
// 1 减少一 2 增加一 3 设置到
// channel:1-A 2-B
// 获取当前频道元素和当前值
let channelStrength = channelIndex === 1 ? channelAStrength : channelBStrength;
// 如果是设置操作
if (type === 3) {
channelStrength = strength; //固定为0
}
// 减少一
else if (type === 1) {
channelStrength = Math.max(channelStrength - strength, 0);
}
// 增加一
else if (type === 2) {
channelStrength = Math.min(channelStrength + strength, 200);
}
// 构造消息对象并发送
const data = { type, strength: channelStrength, message: "set channel", channel: channelIndex };
console.log(data)
sendWsMsg(data);
}
function clearAB(channelIndex) {
const data = { type: 4, message: "clear-" + channelIndex }
sendWsMsg(data);
}
function sendWaveData(timeA, timeB, waveA, waveB) {
if (fangdouSetTimeOut) {
return
}
const msg1 = `A:${waveData[waveA]}`
const msg2 = `B:${waveData[waveB]}`
const data = {
type: "clientMsg", message: msg1, message2: msg2, time1: timeA, time2: timeB
}
sendWsMsg(data)
fangdouSetTimeOut = setTimeout(() => {
clearTimeout(fangdouSetTimeOut)
fangdouSetTimeOut = null
}, fangdou)
}
function closeCoyoteSocket() {
try {
wsConn.close()
}
catch (e) {
notyf.error( {message: "郊狼连接断开失败"} )
console.log(e)
return
}
wsConn = null
notyf.success( {message: "郊狼连接已断开"} )
}
export { createCoyoteSocket, closeCoyoteSocket, sendWsMsg, sendWaveData, addOrIncrease, clearAB, qrcodeSrc, qrcodeShow, channelAStrength, channelBStrength }

133
src/socket/index.ts Normal file
View File

@ -0,0 +1,133 @@
import DanmakuWebSocket from "../assets/danmaku-websocket.min.js"
import { Notyf } from 'notyf'
import { closeCoyoteSocket, addOrIncrease, sendWaveData } from "./coyote"
let ws: DanmakuWebSocket
const notyf = new Notyf({ duration: 4000 })
/**
* socket长连接
* @param authBody
* @param wssLinks
*/
function createSocket(authBody: string, wssLinks: string[]) {
const opt = {
...getWebSocketConfig(authBody, wssLinks),
// 收到消息,
onReceivedMessage: (res) => {
console.log("收到"+ res.cmd +"消息:")
//console.log(res.data.uname + "(大航海" +res.data.guard_level + "级):" + res.data.msg)
// if (res.data.msg == "#UPA1") {
// try {
// addOrIncrease(2, 1, 1)
// notyf.success("A通道强度增加成功")
// }
// catch (e) {
// console.log(e)
// notyf.error("A通道强度增加失败")
// }
// }
if (res.cmd == "LIVE_OPEN_PLATFORM_SEND_GIFT") {
if (res.data.gift_id == 31036) {
// 小花花减强度1
try {
addOrIncrease(1, 1, 1)
addOrIncrease(1, 2, 1)
notyf.success("收到花花,强度-1")
}
catch (e) {
console.log(e)
notyf.error("强度操作失败!")
}
} else if (res.data.gift_id == 31039) {
// 牛哇牛哇加强度1
try {
addOrIncrease(2, 1, 1)
addOrIncrease(2, 2, 1)
notyf.success("收到牛牛,强度+1")
}
catch (e) {
console.log(e)
notyf.error("强度操作失败!")
}
} else {
// 其他礼物,发送波形数据
try {
sendWaveData(5, 5, res.data.gift_id, res.data.gift_id)
notyf.success("收到礼物" + res.data.gift_name)
}
catch (e) {
console.log(e)
notyf.error("发送波形数据失败!")
}
}
}
console.log(res)
},
// 收到心跳处理回调
onHeartBeatReply: (data) => console.log("收到心跳处理回调:", data),
onError: (data) => console.log("error", data),
onListConnectError: () => {
console.log("list connect error")
destroySocket()
},
}
if (!ws) {
ws = new DanmakuWebSocket(opt)
}
return ws
}
/**
* websocket配置信息
* @param authBody
* @param wssLinks
*/
function getWebSocketConfig(authBody: string, wssLinks: string[]) {
const url = wssLinks[0]
const urlList = wssLinks
const auth_body = JSON.parse(authBody)
return {
url,
urlList,
customAuthParam: [
{
key: "key",
value: auth_body.key,
type: "string",
},
{
key: "group",
value: auth_body.group,
type: "string",
},
],
rid: auth_body.roomid,
protover: auth_body.protoover,
uid: auth_body.uid,
}
}
/**
* websocket
*/
function destroySocket() {
console.log("destroy1")
ws && ws.destroy()
ws = undefined
console.log("destroy2")
}
/**
* websocket实例
*/
function getWsClient() {
return ws
}
export { createSocket, destroySocket, getWebSocketConfig, getWsClient }

19
src/types/index.d.ts vendored Normal file
View File

@ -0,0 +1,19 @@
declare interface ISocketData {
// ip地址
ip: number[]
// host地址 可能是ip 也可能是域名。
host: string[]
// 长连使用的请求json体 第三方无需关注内容,建立长连时使用即可。
auth_body: string
// tcp 端口号
tcp_port: number[]
// ws 端口号
ws_port: number[]
// wss 端口号
wss_port: number[]
}

20
tsconfig.json Normal file
View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es2016",
"module": "ESNext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"types": ["node"],
"allowSyntheticDefaultImports": true,
"noImplicitAny": false,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

8
tsconfig.node.json Normal file
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "node"
},
"include": ["vite.config.ts"]
}

17
vite.config.ts Normal file
View File

@ -0,0 +1,17 @@
import { defineConfig } from "vite"
import vue from "@vitejs/plugin-vue"
import { viteCommonjs } from "@originjs/vite-plugin-commonjs"
import * as path from "path"
// https://vitejs.dev/config/
export default defineConfig({
server: {
// https: true
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src")
}
},
plugins: [vue(), viteCommonjs()]
})