WIP: feat(webserver): Reimplement music player
This commit is contained in:
parent
342b6c756a
commit
b1b3c9e3f9
6 changed files with 485 additions and 6 deletions
|
|
@ -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>
|
||||
|
|
|
|||
75
pkgs/packages/webserver/src/app/music_sample.rs
Normal file
75
pkgs/packages/webserver/src/app/music_sample.rs
Normal 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>
|
||||
}
|
||||
}
|
||||
121
pkgs/packages/webserver/src/app/music_sample/ssr_safe.rs
Normal file
121
pkgs/packages/webserver/src/app/music_sample/ssr_safe.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue