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:
parent
fc5f4d7fce
commit
31fa86d7c3
22712
package-lock.json
generated
22712
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
78
src/HUD/Camera/Camera.tsx
Normal 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;
|
||||||
24
src/HUD/Camera/Container.tsx
Normal file
24
src/HUD/Camera/Container.tsx
Normal 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
13
src/HUD/Camera/index.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
153
src/HUD/Camera/mediaStream.ts
Normal file
153
src/HUD/Camera/mediaStream.ts
Normal 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 };
|
||||||
@ -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}
|
||||||
|
|||||||
@ -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%;
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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`),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user