WIP: feat(webserver): Reimplement music player

This commit is contained in:
Tristan Daniël Maat 2025-11-29 23:30:23 +08:00
parent 17ff62f0b9
commit d482b7ab3a
Signed by: tlater
GPG key ID: 02E935006CF2E8E7
6 changed files with 485 additions and 6 deletions

View file

@ -7,10 +7,12 @@ use leptos_router::{
mod homepage;
mod mail;
mod music_sample;
use crate::components::Navbar;
use homepage::HomePage;
use mail::Mail;
use music_sample::MusicSample;
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
@ -49,6 +51,7 @@ pub fn App() -> impl IntoView {
<Routes fallback=|| "Page not found.".into_view()>
<Route path=StaticSegment("") view=HomePage />
<Route path=StaticSegment("mail") view=Mail />
<Route path=StaticSegment("music_sample") view=MusicSample />
</Routes>
</main>
</Router>

View file

@ -0,0 +1,75 @@
#![allow(dead_code, unused_variables)]
use leptos::{logging, prelude::*};
use leptos_meta::{Meta, Title};
use leptos_use::use_event_listener;
use ssr_safe::{MediaPlayer, MediaPlayerError};
mod ssr_safe;
#[component]
fn Controls() -> impl IntoView {
let player: LocalResource<Result<MediaPlayer, MediaPlayerError>> = expect_context();
Effect::new(move || {
let audio_element = if let Some(Ok(p)) = player.get() {
Some(p.audio_element())
} else {
None
};
use_event_listener(audio_element, ssr_safe::media_events::error, |ev| {
logging::error!("{:?}", ev);
});
});
view! {
<div class="notification">
<Suspense fallback=move || "Initializing audio player...">
<ErrorBoundary fallback=|errors| { "Failed to initialize audio player" }>
<div class="level is-mobile">
<div class="level-left">
// The play/pause/etc button
<div class="level-item">
<button class="button" type="button">
<span class="icon is-medium" />
</button>
</div>
</div>
// The title display
<div class="level-item">
{move || {
Ok::<_, MediaPlayerError>(player.get().transpose()?.map(|p| p.get_title()))
}}
</div>
// The artist display
<div class="level-right">
<div class="level-item">Artist</div>
</div>
</div>
</ErrorBoundary>
</Suspense>
</div>
}
}
#[component]
pub fn MusicSample() -> impl IntoView {
let player = LocalResource::new(MediaPlayer::new);
provide_context(player);
view! {
<Meta name="description" content="tlater.net music visualizer sample" />
<Title text="tlater.net music player" />
<section class="hero is-fullheight-with-navbar">
<div class="hero-body p-0">Body</div>
<div class="hero-foot">
<Controls />
</div>
</section>
}
}

View file

@ -0,0 +1,121 @@
use leptos::{ev::EventDescriptor, logging};
use leptos_use::use_event_listener;
use web_sys::EventTarget;
pub const DEFAULT_MP3: &str = "/Mseq_-_Journey.mp3a";
#[derive(Clone)]
pub struct MediaPlayer {
context: web_sys::AudioContext,
audio_element: web_sys::HtmlAudioElement,
}
impl MediaPlayer {
pub async fn new() -> Result<Self, MediaPlayerError> {
let context = web_sys::AudioContext::new()?;
let audio_element = web_sys::HtmlAudioElement::new_with_src(DEFAULT_MP3)?;
let source_node = context.create_media_element_source(&audio_element)?;
let gain_node = context.create_gain()?;
let analyser_node = context.create_analyser()?;
analyser_node.set_fft_size(2048);
analyser_node.set_smoothing_time_constant(0.8);
source_node.connect_with_audio_node(&analyser_node)?;
source_node.connect_with_audio_node(&gain_node)?;
gain_node.connect_with_audio_node(&context.destination())?;
Ok(Self {
context,
audio_element,
})
}
pub fn set_title(&self, title: &str) {
self.audio_element.set_src(title);
}
pub fn get_title(&self) -> String {
// Hardcoded for now, eventually I'll make this a proper
// player again...
"Journey".to_owned()
}
pub fn context(&self) -> EventTarget {
self.context.clone().into()
}
pub fn audio_element(&self) -> EventTarget {
self.audio_element.clone().into()
}
pub fn use_media_event<Ev, F>(&self, event: Ev, handler: F) -> impl Fn() + Clone + Send + Sync + use<Ev, F>
where
F: FnMut(<Ev as EventDescriptor>::EventType) + 'static,
Ev: EventDescriptor + 'static,
{
use_event_listener(self.audio_element.clone(), event, handler)
}
pub fn use_statechange<F>(&self, handler: F) -> impl Fn() + Clone + Send + Sync
where
F: FnMut(<media_events::statechange as EventDescriptor>::EventType) + 'static,
{
use_event_listener(self.context.clone(), media_events::statechange, handler)
}
}
#[derive(thiserror::Error, Debug, Clone)]
pub enum MediaPlayerError {
#[error("todo")]
Todo,
}
impl From<web_sys::wasm_bindgen::JsValue> for MediaPlayerError {
fn from(value: web_sys::wasm_bindgen::JsValue) -> Self {
logging::error!("Some kind of error");
Self::Todo {}
}
}
pub mod media_events {
use leptos::ev::EventDescriptor;
use std::borrow::Cow;
#[derive(Copy, Clone, Debug)]
#[allow(non_camel_case_types)]
pub struct error;
impl EventDescriptor for error {
type EventType = web_sys::Event;
const BUBBLES: bool = false;
#[inline(always)]
fn name(&self) -> Cow<'static, str> {
"error".into()
}
#[inline(always)]
fn event_delegation_key(&self) -> Cow<'static, str> {
"$$$error".into()
}
}
#[derive(Copy, Clone, Debug)]
#[allow(non_camel_case_types)]
pub struct statechange;
impl EventDescriptor for statechange {
type EventType = web_sys::Event;
const BUBBLES: bool = false;
#[inline(always)]
fn name(&self) -> Cow<'static, str> {
"statechange".into()
}
#[inline(always)]
fn event_delegation_key(&self) -> Cow<'static, str> {
"$$$statechange".into()
}
}
}