1
0
mirror of https://github.com/lexogrine/dota2-react-hud.git synced 2025-12-10 01:52:49 +01:00

added camera system

This commit is contained in:
Hubert Walczak 2021-12-08 21:24:47 +01:00
parent fc5f4d7fce
commit 31fa86d7c3
No known key found for this signature in database
GPG Key ID: 17BB1C9355357860
14 changed files with 32396 additions and 10197 deletions

22712
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "lexogrine_dota2_hud", "name": "lexogrine_dota2_hud",
"version": "1.1.0", "version": "1.2.0",
"homepage": "./", "homepage": "./",
"private": true, "private": true,
"dependencies": { "dependencies": {
@ -14,8 +14,10 @@
"react": "^16.13.1", "react": "^16.13.1",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-scripts": "4.0.0", "react-scripts": "4.0.0",
"simple-peer": "^9.11.0",
"socket.io-client": "^4.1.3", "socket.io-client": "^4.1.3",
"typescript": "3.6.4" "typescript": "3.6.4",
"uuid": "^8.3.2"
}, },
"license": "GPL-3.0", "license": "GPL-3.0",
"scripts": { "scripts": {
@ -44,9 +46,12 @@
}, },
"devDependencies": { "devDependencies": {
"@types/history": "^4.7.5", "@types/history": "^4.7.5",
"@types/simple-peer": "^9.11.3",
"@types/socket.io-client": "^1.4.32",
"@types/uuid": "^8.3.3",
"internal-ip": "^6.2.0", "internal-ip": "^6.2.0",
"npm-build-zip": "^1.0.2",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"npm-build-zip": "^1.0.2",
"open": "^8.0.2", "open": "^8.0.2",
"sass": "^1.32.5" "sass": "^1.32.5"
} }

View File

@ -1,6 +1,6 @@
{ {
"name":"Lexogrine Dota2 HUD", "name":"Lexogrine Dota2 HUD",
"version":"1.1.0", "version":"1.2.0",
"author":"Lexogrine", "author":"Lexogrine",
"legacy": false, "legacy": false,
"radar": true, "radar": true,

View File

@ -10,9 +10,10 @@ import Statistics from './HUD/GameHUD/ObservedStatistics';
import TopSideBar from './HUD/GameHUD/TopSideBar'; import TopSideBar from './HUD/GameHUD/TopSideBar';
import "./HUD/GameHUD/gamehud.scss"; import "./HUD/GameHUD/gamehud.scss";
import { exampleData } from './example'; import { exampleData } from './example';
import { initiateConnection } from './HUD/Camera/mediaStream';
const DOTA2 = new DOTA2GSI(); const DOTA2 = new DOTA2GSI();
const socket = io(isDev ? `localhost:${port}` : '/'); export const socket = io(isDev ? `localhost:${port}` : '/');
const isTest = false; const isTest = false;
@ -116,6 +117,7 @@ class App extends React.Component<any, { game: Dota2 | null, steamids: string[],
socket.on("readyToRegister", () => { socket.on("readyToRegister", () => {
socket.emit("register", name, isDev, 'dota2'); socket.emit("register", name, isDev, 'dota2');
initiateConnection();
}); });
socket.on(`hud_config`, (data: any) => { socket.on(`hud_config`, (data: any) => {
configs.save(data); configs.save(data);

78
src/HUD/Camera/Camera.tsx Normal file
View File

@ -0,0 +1,78 @@
import React, { useEffect, useState } from "react";
import { mediaStreams } from "./mediaStream";
import { v4 as uuidv4 } from 'uuid';
type Props = {
steamid: string,
visible: boolean;
}
const CameraView = ({ steamid, visible }: Props) => {
const [uuid] = useState(uuidv4());
const [ forceHide, setForceHide ] = useState(false);
useEffect(() => {
}, [])
useEffect(() => {
const mountStream = (stream: MediaStream) => {
console.log("mounting video");
const remoteVideo = document.getElementById(`remote-video-${steamid}-${uuid}`) as HTMLVideoElement;
if(!remoteVideo || !stream){
console.log("no video element")
}
if (!remoteVideo || !stream) return;
remoteVideo.srcObject = stream;
remoteVideo.play();
}
const mountExistingStream = () => {
const currentStream = mediaStreams.players.find(player => player.steamid === steamid);
if(!currentStream || !currentStream.peerConnection || !currentStream.peerConnection._remoteStreams) return;
const stream = currentStream.peerConnection._remoteStreams[0];
if(!stream) return;
mountStream(stream);
}
const onStreamCreate = (stream: MediaStream) => {
mountStream(stream);
}
const onStreamDestroy = () => {
const remoteVideo = document.getElementById(`remote-video-${steamid}-${uuid}`) as HTMLVideoElement;
if (!remoteVideo) return;
remoteVideo.srcObject = null;
}
const onBlockedUpdate = (steamids: string[]) => {
setForceHide(steamids.includes(steamid));
}
mediaStreams.onStreamCreate(onStreamCreate, steamid);
mediaStreams.onStreamDestroy(onStreamDestroy, steamid);
mediaStreams.onBlockedUpdate(onBlockedUpdate);
mountExistingStream();
return () => {
mediaStreams.removeListener(onStreamCreate);
mediaStreams.removeListener(onStreamDestroy);
mediaStreams.removeListener(onBlockedUpdate);
}
}, [steamid]);
return <React.Fragment>
<video className="video-call-preview" autoPlay muted id={`remote-video-${steamid}-${uuid}`} style={{ opacity: visible && !forceHide ? 1 : 0.001 }}></video>
</React.Fragment>
}
export default CameraView;

View File

@ -0,0 +1,24 @@
import React, { useEffect, useState } from "react";
import PlayerCamera from "./Camera";
import api from "../../api/api";
import "./index.scss";
const CameraContainer = ({ observedSteamid }: { observedSteamid: string | null }) => {
const [ players, setPlayers ] = useState<string[]>([]);
useEffect(() => {
api.camera.get().then(response => {
setPlayers(response.availablePlayers.map(player => player.steamid));
});
}, []);
return <div id="cameras-container">
{
players.map(steamid => (<PlayerCamera key={steamid} steamid={steamid} visible={observedSteamid === steamid} />))
}
</div>
}
export default CameraContainer;

13
src/HUD/Camera/index.scss Normal file
View File

@ -0,0 +1,13 @@
#cameras-container {
overflow: hidden;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
video {
height: 100%;
position: absolute;
}
}

View File

@ -0,0 +1,153 @@
import { Instance, SignalData } from 'simple-peer';
import api from '../../api/api';
import { socket as Socket } from "../../App";
const Peer = require('simple-peer');
const wait = (ms: number) => new Promise(r => setTimeout(r, ms));
type OfferData = {
offer: SignalData,
}
type PeerInstance = Instance & { _remoteStreams: MediaStream[] }
type MediaStreamPlayer = {
peerConnection: PeerInstance | null;
steamid: string;
}
type ListenerType = ({ listener: (stream: MediaStream) => void, event: 'create', steamid: string } | ({ listener: () => void, event: 'destroy', steamid: string }));
type MediaStreamManager = {
blocked: string[];
blockedListeners: ((blocked: string[]) => void)[],
players: MediaStreamPlayer[];
onStreamCreate: (listener: (stream: MediaStream) => void, steamid: string) => void;
onStreamDestroy: (listener: () => void, steamid: string) => void;
onBlockedUpdate: (listener: (steamids: string[]) => void) => void;
removeListener: (listener: any) => void;
listeners: ListenerType[];
}
const mediaStreams: MediaStreamManager = {
blocked: [],
blockedListeners: [],
players: [],
listeners: [],
onStreamCreate: (listener: (stream: MediaStream) => void, steamid: string) => {
mediaStreams.listeners.push({ listener, event: "create", steamid });
},
onBlockedUpdate: (listener: (blocked: string[]) => void) => {
mediaStreams.blockedListeners.push(listener);
},
onStreamDestroy: (listener: () => void, steamid: string) => {
mediaStreams.listeners.push({ listener, event: "destroy", steamid });
},
removeListener: (listenerToRemove: any) => {
mediaStreams.listeners = mediaStreams.listeners.filter(listener => listener !== listenerToRemove);
mediaStreams.blockedListeners = mediaStreams.blockedListeners.filter(listener => listener !== listenerToRemove);
}
};
const getConnectionInfo = (steamid: string) => mediaStreams.players.find(player => player.steamid === steamid) || null;
const closeConnection = (steamid: string) => {
const connectionInfo = getConnectionInfo(steamid);
try {
if(connectionInfo){
if(connectionInfo.peerConnection){
connectionInfo.peerConnection.removeAllListeners();
connectionInfo.peerConnection.destroy();
}
connectionInfo.peerConnection = null;
}
} catch {
}
for(const listener of mediaStreams.listeners.filter(listener => listener.steamid === steamid)){
if(listener.event === "destroy") listener.listener();
}
mediaStreams.players = mediaStreams.players.filter(player => player.steamid !== steamid);
console.log(mediaStreams.players)
}
const initiateConnection = async () => {
const socket = Socket as SocketIOClient.Socket;
const camera = await api.camera.get();
await wait(1000);
socket.emit("registerAsHUD", camera.uuid);
socket.on('playersCameraStatus', (players: { steamid: string, label: string, allow: boolean, active: boolean }[]) => {
const blockedSteamids = players.filter(player => !player.allow).map(player => player.steamid);
mediaStreams.blocked = blockedSteamids;
for(const listener of mediaStreams.blockedListeners){
listener(blockedSteamids);
}
});
socket.on('offerFromPlayer', async (roomId: string, offerData: OfferData, steamid: string) => {
// It's not from available player, ignore incoming request
/*if(!camera.availablePlayers.find(player => player.steamid === steamid)){
console.log("Wrong player");
return;
}*/
const currentConnection = getConnectionInfo(steamid);
// Connection already made, ignore incoming request
if(currentConnection){
console.log("Connection has been made already");
return;
}
if (camera.uuid !== roomId) return;
const peerConnection: PeerInstance = new Peer({ initiator: false, trickle: false });
const mediaStreamPlayer: MediaStreamPlayer = { peerConnection, steamid };
mediaStreams.players.push(mediaStreamPlayer);
peerConnection.on('signal', answer => {
console.log("SIGNAL COMING IN");
const offer = JSON.parse(JSON.stringify(answer)) as RTCSessionDescriptionInit;
socket.emit("offerFromHUD", roomId, { offer }, steamid);
});
peerConnection.on('error', (err) => {
console.log(err)
closeConnection(steamid);
});
peerConnection.on('stream', () => {
console.log("STREAM COMING IN");
const currentConnection = getConnectionInfo(steamid);
if(!currentConnection){
console.log("Connection not established");
closeConnection(steamid);
return;
}
if(peerConnection._remoteStreams.length === 0){
console.log('no stream?');
return;
}
for(const listener of mediaStreams.listeners.filter(listener => listener.steamid === steamid)){
if(listener.event === "create") listener.listener(peerConnection._remoteStreams[0]);
}
});
peerConnection.on('close', () => {
console.log("CLOSE COMING IN");
const currentConnection = getConnectionInfo(steamid);
if (!currentConnection) return;
closeConnection(steamid);
});
console.log("Sending offer");
peerConnection.signal(offerData.offer);
});
}
export { mediaStreams, initiateConnection };

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { Draft, Team, Faction, Player, TeamDraft } from 'dotagsi'; import { Draft, Team, Faction, Player, TeamDraft } from 'dotagsi';
import { apiUrl } from '../../api/api'; import { apiUrl } from '../../api/api';
import CameraContainer from '../Camera/Container';
const ObservedPlayer = ({ players, player, team, show}: { show: boolean, player: Player | null, players: Player[], team: Team | null }) => { const ObservedPlayer = ({ players, player, team, show}: { show: boolean, player: Player | null, players: Player[], team: Team | null }) => {
const getPlayerById = (id: number) => { const getPlayerById = (id: number) => {
@ -30,6 +31,7 @@ const ObservedPlayer = ({ players, player, team, show}: { show: boolean, player:
</div> </div>
<div className="player_picture"> <div className="player_picture">
<CameraContainer observedSteamid={player.steamid} />
{player.avatar ? <img src={player.avatar} /> : null} {player.avatar ? <img src={player.avatar} /> : null}
</div> </div>
</div> : null} </div> : null}

View File

@ -137,6 +137,7 @@ body {
justify-content: center; justify-content: center;
background-color: rgba(1, 3, 20, 1); background-color: rgba(1, 3, 20, 1);
align-items: flex-end; align-items: flex-end;
position: relative;
img { img {
max-height: 100%; max-height: 100%;

View File

@ -96,6 +96,7 @@ export default class Layout extends React.Component<Props, State> {
activeTeamBonusTime = game.draft.dire.bonus_time; activeTeamBonusTime = game.draft.dire.bonus_time;
} }
} }
return ( return (
<> <>
<div className="layout"> <div className="layout">

View File

@ -1,11 +1,11 @@
import { Player } from "dotagsi"; import { Player } from "dotagsi";
import React from "react"; import React from "react";
import Ability from "../Ability"; import Ability from "../Ability";
import CameraContainer from "../Camera/Container";
import "./observed.scss"; import "./observed.scss";
export default class Observed extends React.Component<{ player: Player | null }> { export default class Observed extends React.Component<{ player: Player | null }> {
render() { render() {
const { player } = this.props; const { player } = this.props;
if (!player || !player.hero) return null;; if (!player || !player.hero) return null;;
@ -15,9 +15,9 @@ export default class Observed extends React.Component<{ player: Player | null }>
player.hero.name ? <div className="main_row"> player.hero.name ? <div className="main_row">
<div className={`avatar`}> <div className={`avatar`}>
<img src={`./heroes/${player.hero.name.replace('npc_dota_hero_', '')}.png`} width={140} alt={'Avatar'} /> <img src={`./heroes/${player.hero.name.replace('npc_dota_hero_', '')}.png`} width={140} alt={'Avatar'} />
</div> </div>
<div className="username_container"> <div className="username_container">
<div className="username">[{player.hero.name.replace('npc_dota_hero_', '').toUpperCase()}] {player.name}, level {player.hero.level}</div> <div className="username">[{player.hero.name.replace('npc_dota_hero_', '').toUpperCase()}] {player.name}, level {player.hero.level}</div>

View File

@ -31,6 +31,9 @@ const api = {
get: async (): Promise<I.Match[]> => apiV2(`match`), get: async (): Promise<I.Match[]> => apiV2(`match`),
getCurrent: async (): Promise<I.Match> => apiV2(`match/current`) getCurrent: async (): Promise<I.Match> => apiV2(`match/current`)
}, },
camera: {
get: (): Promise<{ availablePlayers: ({steamid:string, label: string})[], uuid: string }> => apiV2('camera')
},
teams: { teams: {
getOne: async (id: string): Promise<I.Team> => apiV2(`teams/${id}`), getOne: async (id: string): Promise<I.Team> => apiV2(`teams/${id}`),
get: (): Promise<I.Team[]> => apiV2(`teams`), get: (): Promise<I.Team[]> => apiV2(`teams`),

19585
yarn.lock

File diff suppressed because it is too large Load Diff