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",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"homepage": "./",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
@ -14,8 +14,10 @@
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-scripts": "4.0.0",
|
||||
"simple-peer": "^9.11.0",
|
||||
"socket.io-client": "^4.1.3",
|
||||
"typescript": "3.6.4"
|
||||
"typescript": "3.6.4",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"scripts": {
|
||||
@ -44,9 +46,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"npm-build-zip": "^1.0.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"npm-build-zip": "^1.0.2",
|
||||
"open": "^8.0.2",
|
||||
"sass": "^1.32.5"
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name":"Lexogrine Dota2 HUD",
|
||||
"version":"1.1.0",
|
||||
"version":"1.2.0",
|
||||
"author":"Lexogrine",
|
||||
"legacy": false,
|
||||
"radar": true,
|
||||
|
||||
@ -10,9 +10,10 @@ import Statistics from './HUD/GameHUD/ObservedStatistics';
|
||||
import TopSideBar from './HUD/GameHUD/TopSideBar';
|
||||
import "./HUD/GameHUD/gamehud.scss";
|
||||
import { exampleData } from './example';
|
||||
import { initiateConnection } from './HUD/Camera/mediaStream';
|
||||
|
||||
const DOTA2 = new DOTA2GSI();
|
||||
const socket = io(isDev ? `localhost:${port}` : '/');
|
||||
export const socket = io(isDev ? `localhost:${port}` : '/');
|
||||
|
||||
const isTest = false;
|
||||
|
||||
@ -116,6 +117,7 @@ class App extends React.Component<any, { game: Dota2 | null, steamids: string[],
|
||||
|
||||
socket.on("readyToRegister", () => {
|
||||
socket.emit("register", name, isDev, 'dota2');
|
||||
initiateConnection();
|
||||
});
|
||||
socket.on(`hud_config`, (data: any) => {
|
||||
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 { Draft, Team, Faction, Player, TeamDraft } from 'dotagsi';
|
||||
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 getPlayerById = (id: number) => {
|
||||
@ -30,6 +31,7 @@ const ObservedPlayer = ({ players, player, team, show}: { show: boolean, player:
|
||||
|
||||
</div>
|
||||
<div className="player_picture">
|
||||
<CameraContainer observedSteamid={player.steamid} />
|
||||
{player.avatar ? <img src={player.avatar} /> : null}
|
||||
</div>
|
||||
</div> : null}
|
||||
|
||||
@ -137,6 +137,7 @@ body {
|
||||
justify-content: center;
|
||||
background-color: rgba(1, 3, 20, 1);
|
||||
align-items: flex-end;
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
max-height: 100%;
|
||||
|
||||
@ -96,6 +96,7 @@ export default class Layout extends React.Component<Props, State> {
|
||||
activeTeamBonusTime = game.draft.dire.bonus_time;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="layout">
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { Player } from "dotagsi";
|
||||
import React from "react";
|
||||
import Ability from "../Ability";
|
||||
import CameraContainer from "../Camera/Container";
|
||||
import "./observed.scss";
|
||||
|
||||
|
||||
export default class Observed extends React.Component<{ player: Player | null }> {
|
||||
|
||||
render() {
|
||||
const { player } = this.props;
|
||||
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">
|
||||
|
||||
<div className={`avatar`}>
|
||||
|
||||
<img src={`./heroes/${player.hero.name.replace('npc_dota_hero_', '')}.png`} width={140} alt={'Avatar'} />
|
||||
|
||||
|
||||
</div>
|
||||
<div className="username_container">
|
||||
<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`),
|
||||
getCurrent: async (): Promise<I.Match> => apiV2(`match/current`)
|
||||
},
|
||||
camera: {
|
||||
get: (): Promise<{ availablePlayers: ({steamid:string, label: string})[], uuid: string }> => apiV2('camera')
|
||||
},
|
||||
teams: {
|
||||
getOne: async (id: string): Promise<I.Team> => apiV2(`teams/${id}`),
|
||||
get: (): Promise<I.Team[]> => apiV2(`teams`),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user