Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/audio stream #1483

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions client/src/apiTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ export interface ApiAssetUpload {
totalSlices: number;
data: string;
}
export interface ApiAudioMessage {
action: string;
fileName: string;
}
export interface ApiBaseRectShape extends ApiCoreShape {
width: number;
height: number;
Expand Down
1 change: 1 addition & 0 deletions client/src/game/api/events.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import "../systems/access/events";
import "../systems/audio/events";
import "../systems/auras/events";
import "../systems/characters/events";
import "../systems/chat/events";
Expand Down
6 changes: 6 additions & 0 deletions client/src/game/systems/audio/emits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { ApiAudioMessage } from "../../../apiTypes";
import { wrapSocket } from "../../api/helpers";

export const sendPlayCommand = wrapSocket<ApiAudioMessage>("Audio.Play");
export const sendStopCommand = wrapSocket<ApiAudioMessage>("Audio.Stop");
export const toggleLoopCommand = wrapSocket<ApiAudioMessage>("Audio.ToggleLoop");
14 changes: 14 additions & 0 deletions client/src/game/systems/audio/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { ApiAudioMessage } from "../../../apiTypes";
import { socket } from "../../api/socket";

import { audioSystem } from ".";

socket.on("Audio.Play", (data: ApiAudioMessage) => {
audioSystem.StartPlayback(data.action, data.fileName);
});
socket.on("Audio.Stop", (data: ApiAudioMessage) => {
audioSystem.StopPlayback(data.action, data.fileName);
});
socket.on("Audio.ToggleLoop", (data: ApiAudioMessage) => {
audioSystem.ToggleLoopPlayback(data.action, data.fileName);
});
39 changes: 39 additions & 0 deletions client/src/game/systems/audio/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { type System, registerSystem } from "..";
import type { SystemClearReason } from "../models";

import { sendPlayCommand } from "./emits";

Check warning on line 4 in client/src/game/systems/audio/index.ts

View workflow job for this annotation

GitHub Actions / CLIENT-lint

'/home/runner/work/PlanarAlly/PlanarAlly/client/src/game/systems/audio/emits.ts' imported multiple times
import { sendStopCommand } from "./emits";

Check warning on line 5 in client/src/game/systems/audio/index.ts

View workflow job for this annotation

GitHub Actions / CLIENT-lint

'/home/runner/work/PlanarAlly/PlanarAlly/client/src/game/systems/audio/emits.ts' imported multiple times
import { toggleLoopCommand } from "./emits";

Check warning on line 6 in client/src/game/systems/audio/index.ts

View workflow job for this annotation

GitHub Actions / CLIENT-lint

'/home/runner/work/PlanarAlly/PlanarAlly/client/src/game/systems/audio/emits.ts' imported multiple times

// import MenuBar from "../../ui/menu/MenuBar.vue";
import { audioService } from "../../ui/menu/audioService";

Check failure on line 9 in client/src/game/systems/audio/index.ts

View workflow job for this annotation

GitHub Actions / CLIENT-lint

`../../ui/menu/audioService` import should occur before type import of `../models`

class AudioSystem implements System {
clear(reason: SystemClearReason): void {
}

PlayAudioForRoom(action: string, fileName: string): void {
sendPlayCommand({ action, fileName});
}

StopAudioForRoom(action: string): void {
sendStopCommand({ action, fileName: ""});
}

ToggleLoopAudioForRoom(action: string): void {
toggleLoopCommand({ action, fileName: ""});
}

StartPlayback(action: string, fileName: string): void {
audioService.startPlayback(fileName);
}
StopPlayback(action: string, fileName: string): void {
audioService.stopPlayback();
}
ToggleLoopPlayback(action: string, fileName: string): void {
audioService.toggleLoopPlayback();
}
}

export const audioSystem = new AudioSystem();
registerSystem("audio", audioSystem, false);
4 changes: 4 additions & 0 deletions client/src/game/systems/audio/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface AudioMessage {

Check failure on line 1 in client/src/game/systems/audio/model.ts

View workflow job for this annotation

GitHub Actions / CLIENT-lint

exported declaration 'AudioMessage' not used within other modules
action: string;
fileName: string;
}
152 changes: 151 additions & 1 deletion client/src/game/ui/menu/MenuBar.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref, toRef } from "vue";
import { computed, onMounted, ref, toRef } from "vue";
import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router";

Expand All @@ -21,7 +21,9 @@
import { uiState } from "../../systems/ui/state";

import AssetParentNode from "./AssetParentNode.vue";
import Characters from "./Characters.vue";

Check failure on line 24 in client/src/game/ui/menu/MenuBar.vue

View workflow job for this annotation

GitHub Actions / CLIENT-lint

There should be at least one empty line between import groups
import { audioSystem } from "../../systems/audio";

Check failure on line 25 in client/src/game/ui/menu/MenuBar.vue

View workflow job for this annotation

GitHub Actions / CLIENT-lint

There should be at least one empty line between import groups

Check failure on line 25 in client/src/game/ui/menu/MenuBar.vue

View workflow job for this annotation

GitHub Actions / CLIENT-lint

`../../systems/audio` import should occur before import of `../../systems/client`
import { audioService } from './audioService';

Check failure on line 26 in client/src/game/ui/menu/MenuBar.vue

View workflow job for this annotation

GitHub Actions / CLIENT-lint

`./audioService` import should occur before import of `./Characters.vue`

const router = useRouter();
const { t } = useI18n();
Expand Down Expand Up @@ -85,6 +87,94 @@

const openDmSettings = (): void => uiSystem.showDmSettings(!uiState.raw.showDmSettings);
const openClientSettings = (): void => uiSystem.showClientSettings(!uiState.raw.showClientSettings);
const openLgSettings = (): void => uiSystem.showLgSettings(!uiState.raw.showLgSettings);

Check failure on line 90 in client/src/game/ui/menu/MenuBar.vue

View workflow job for this annotation

GitHub Actions / CLIENT-lint

Unsafe return of an `any` typed value

Check failure on line 90 in client/src/game/ui/menu/MenuBar.vue

View workflow job for this annotation

GitHub Actions / CLIENT-lint

Unsafe call of an `any` typed value

Check failure on line 90 in client/src/game/ui/menu/MenuBar.vue

View workflow job for this annotation

GitHub Actions / CLIENT-lint

Unexpected any value in conditional. An explicit comparison or type cast is required

// Ref to store available mp3 assets
const audioAssets = ref<AssetFile[]>([]);
// Ref to store the selected audio path
const selectedAudioHash = ref("");
const selectedPreviewAudioHash = ref("");
var isAudioLooping = ref(false);

const getAssetPath = (assetHash: string): string => {
return `/static/assets/${assetHash}`;
};

// Fetch assets from the game state and filter them by mp3 type
const loadAudioAssets = () => {

Check failure on line 104 in client/src/game/ui/menu/MenuBar.vue

View workflow job for this annotation

GitHub Actions / CLIENT-lint

Missing return type on function
const assets = gameState.reactive.assets.get("__files") as AssetFile[] || [];
audioAssets.value = assets.filter(asset => asset.name.endsWith(".mp3"));

// Set the first asset as the default selected one
// if (audioAssets.value.length > 0) {
// selectedPreviewAudioHash.value = getAssetPath(audioAssets.value[0].hash); // Utilise la fonction getAssetPath
// }
};

// Function to play the selected audio
const playPreviewAudio = (hash: string) => {
const audioElement = document.getElementById("AdminPreviewAudioPlayer") as HTMLAudioElement;
const sourceElement = document.getElementById("AdminPreviewAudioSource") as HTMLSourceElement;
sourceElement.src = getAssetPath(hash);
audioElement.load();
audioElement.play();
};

// Call the function to load audio assets when the component is mounted
onMounted(() => {
loadAudioAssets();
// Initialisation de audioPlayer et audioSource après que le DOM est monté
audioService.audioPlayer = document.getElementById("clientAudioPlayer") as HTMLAudioElement;
audioService.audioSource = document.getElementById("clientAudioSource") as HTMLSourceElement;

if (!audioService.audioPlayer || !audioService.audioSource) {
console.error("Audio player or audio source not found in the DOM");
}
});
///// from DM /////
function playAudioForRoom(action: string, fileName: string): void {
audioSystem.PlayAudioForRoom(action, fileName);
}
function stopAudioForRoom(action: string): void {
audioSystem.StopAudioForRoom(action);
}
function toggleLoopAudioForRoom(action: string): void {
isAudioLooping.value = !isAudioLooping.value;
audioSystem.ToggleLoopAudioForRoom(action);
}
///// end from DM /////

///// from Player /////
// // Références au lecteur audio et à la source
// const clientAudioPlayer = document.getElementById("clientAudioPlayer") as HTMLAudioElement;
// const clientAudioSource = document.getElementById("clientAudioSource") as HTMLAudioElement;

// // Méthode pour démarrer la lecture de l'audio
// function startPlayback(file: string) {
// clientAudioSource.src = getAssetPath(file);
// clientAudioPlayer.load(); // Recharge la nouvelle source
// clientAudioPlayer.play(); // Lance la lecture
// }

// // Méthode pour arrêter l'audio
// function stopPlayback() {
// clientAudioPlayer.pause();
// clientAudioPlayer.currentTime = 0; // Réinitialise la lecture
// }

// // Méthode pour activer/désactiver la lecture en boucle
// function toggleLoopPlayback() {
// clientAudioPlayer.loop = !clientAudioPlayer.loop;
// }

// // Exposer les méthodes pour les appeler depuis `index.ts`
// defineExpose({
// startPlayback,
// stopPlayback,
// toggleLoopPlayback,
// });

///// END from Player /////
</script>

<template>
Expand Down Expand Up @@ -158,6 +248,61 @@
<button class="menu-accordion" @click="openClientSettings">
{{ t("game.ui.menu.MenuBar.client_settings") }}
</button>

<!-- AUDIO PLAYER-->

<button class="menu-accordion">
{{ t("game.ui.menu.MenuBar.audio_player") }} <!-- ou bien : "Audio" directement -->
</button>
<div id="menu-audio" class="menu-accordion-panel" style="position: relative">
<!-- Audio panel content -->
<template v-if="gameState.isDmOrFake.value">
<p>Preview : </p>
<p>
<select class="audio-perview-select" v-model="selectedPreviewAudioHash">

Check warning on line 262 in client/src/game/ui/menu/MenuBar.vue

View workflow job for this annotation

GitHub Actions / CLIENT-lint

Attribute "v-model" should go before "class"
<option v-for="asset in audioAssets" :key="asset.id" :value="asset.hash">
{{ asset.name }}
</option>
</select>
<button class="audio-preview-button" @click="playPreviewAudio(selectedPreviewAudioHash)">Play</button>
</p>
<audio id="AdminPreviewAudioPlayer" controls style="width: 100%;">
<source id="AdminPreviewAudioSource" src="" type="audio/mpeg">
Your browser does not support the audio element.
</audio>

<div class="audio-player">
<p class="audio-title">Now Playing</p>



<div class="audio-selector">
<p>
<select class="audio-perview-select" v-model="selectedAudioHash">

Check warning on line 281 in client/src/game/ui/menu/MenuBar.vue

View workflow job for this annotation

GitHub Actions / CLIENT-lint

Attribute "v-model" should go before "class"
<option v-for="asset in audioAssets" :key="asset.id" :value="asset.hash">
{{ asset.name }}
</option>
</select>
</p>
</div>
<div class="audio-controls">
<button class="play-button" @click="playAudioForRoom('play', selectedAudioHash)">Play</button>
<button class="stop-button" @click="stopAudioForRoom('stop')">Stop</button>
<button class="loop-button" @click="toggleLoopAudioForRoom('toggleLoop')">Loop {{ isAudioLooping ? 'OFF' : 'ON' }}</button>
</div>
</div>
</template>

<!-- Audio player for clients -->
<template v-if="!gameState.isDmOrFake.value">
<audio id="clientAudioPlayer" controls >
<source id="clientAudioSource" src="" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
</template>
</div>

<!-- END AUDIO PLAYER -->
</div>
<div
class="menu-accordion"
Expand All @@ -175,6 +320,11 @@
flex-direction: column;
}

.menu-accordion-active + #menu-audio {
display: flex;
flex-direction: column;
}

#asset-search {
text-align: center;

Expand Down
30 changes: 30 additions & 0 deletions client/src/game/ui/menu/audioService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export class AudioService {
public audioPlayer: HTMLAudioElement;
public audioSource: HTMLSourceElement;

constructor() {
this.audioPlayer = document.getElementById("clientAudioPlayer") as HTMLAudioElement;
this.audioSource = document.getElementById("clientAudioSource") as HTMLSourceElement;
}

getAssetPath = (assetHash: string): string => {
return `/static/assets/${assetHash}`;
};

startPlayback(file: string): void {
this.audioSource.src = this.getAssetPath(file);
this.audioPlayer.load();
this.audioPlayer.play();
}

stopPlayback(): void {
this.audioPlayer.pause();
this.audioPlayer.currentTime = 0;
}

toggleLoopPlayback(): void {
this.audioPlayer.loop = !this.audioPlayer.loop;
}
}

export const audioService = new AudioService();
4 changes: 3 additions & 1 deletion client/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@
"no_notes": "No notes",
"dm_settings": "DM Settings",
"no_markers": "No markers",
"client_settings": "Client Settings"
"client_settings": "Client Settings",
"audio_player": "Audio Player",
"gameboard_settings": "LG Settings"
}
},
"NoteDialog": {
Expand Down
6 changes: 6 additions & 0 deletions server/src/api/models/audio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from pydantic import BaseModel


class ApiAudioMessage(BaseModel):
action: str
fileName: str
42 changes: 42 additions & 0 deletions server/src/api/socket/audio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from aiohttp import web
from ...app import sio, app
from ... import auth
from ...api.socket.constants import GAME_NS
from ...state.game import game_state
from ...db.models.player_room import PlayerRoom
from typing import Any
from ..models.audio import ApiAudioMessage
from ..helpers import _send_game

# Dictionary to keep track of current audio for each room

@sio.on("Audio.Play", namespace=GAME_NS)
@auth.login_required(app, sio, "game")
async def play_audio(sid: str, raw_data: Any):
"""
Handles the request to play an audio file in a specific room.
The DM will send the audio file ID and room name.
"""
pr: PlayerRoom = game_state.get(sid)
data = ApiAudioMessage(**raw_data)
await _send_game("Audio.Play", data, room=pr.room.get_path(), skip_sid=sid)

@sio.on("Audio.Stop", namespace=GAME_NS)
@auth.login_required(app, sio, "game")
async def stop_audio(sid: str, raw_data: Any):
"""
Handles the request to stop the audio in a specific room.
"""
pr: PlayerRoom = game_state.get(sid)
data = ApiAudioMessage(**raw_data)
await _send_game("Audio.Stop", data, room=pr.room.get_path(), skip_sid=sid)

@sio.on("Audio.ToggleLoop", namespace=GAME_NS)
@auth.login_required(app, sio, "game")
async def loop_audio(sid: str, raw_data: Any):
"""
Handles the request to toggle looping audio in a specific room.
"""
pr: PlayerRoom = game_state.get(sid)
data = ApiAudioMessage(**raw_data)
await _send_game("Audio.ToggleLoop", data, room=pr.room.get_path(), skip_sid=sid)
Loading