Surn's picture
Add background music feature with controls
7ef6f2b
raw
history blame
4.68 kB
import os
from typing import Optional
import streamlit as st
def _get_audio_dir() -> str:
return os.path.join(os.path.dirname(__file__), "assets", "audio")
def get_audio_tracks() -> list[tuple[str, str]]:
"""Return list of (label, absolute_path) for .mp3 files in assets/audio."""
audio_dir = _get_audio_dir()
if not os.path.isdir(audio_dir):
return []
files = [f for f in os.listdir(audio_dir) if f.lower().endswith(".mp3")]
files.sort()
return [(os.path.splitext(f)[0].replace("_", " ").title(), os.path.join(audio_dir, f)) for f in files]
@st.cache_data(show_spinner=False)
def _load_audio_data_url(path: str) -> str:
"""Return a data: URL for the given audio file so the browser can play it."""
import base64, mimetypes
mime, _ = mimetypes.guess_type(path)
if not mime:
mime = "audio/mpeg"
with open(path, "rb") as fp:
encoded = base64.b64encode(fp.read()).decode("ascii")
return f"data:{mime};base64,{encoded}"
def _mount_background_audio(enabled: bool, src_data_url: Optional[str], volume: float) -> None:
"""Create/update a single hidden <audio> element in the top page and play/pause it."""
from streamlit.components.v1 import html as _html
if not enabled or not src_data_url:
_html(
"""
<script>
(function(){
const doc = window.parent?.document || document;
const el = doc.getElementById('bw-bg-audio');
if (el) { try { el.pause(); } catch(e){} }
})();
</script>
""",
height=0,
)
return
# Clamp volume
vol = max(0.0, min(1.0, float(volume)))
# Inject or update a single persistent audio element and make sure it starts after interaction if autoplay is blocked
_html(
f"""
<script>
(function(){{
const doc = window.parent?.document || document;
let audio = doc.getElementById('bw-bg-audio');
if (!audio) {{
audio = doc.createElement('audio');
audio.id = 'bw-bg-audio';
audio.style.display = 'none';
audio.setAttribute('loop', '');
audio.setAttribute('autoplay', '');
doc.body.appendChild(audio);
}}
const newSrc = "{src_data_url}";
if (audio.src !== newSrc) {{
audio.src = newSrc;
}}
audio.muted = false;
audio.volume = {vol:.3f};
const tryPlay = () => {{
const p = audio.play();
if (p && p.catch) {{ p.catch(() => {{ /* ignore autoplay block until user gesture */ }}); }}
}};
tryPlay();
const unlock = () => {{
tryPlay();
}};
// Add once-only listeners to resume playback after first user interaction
doc.addEventListener('pointerdown', unlock, {{ once: true }});
doc.addEventListener('keydown', unlock, {{ once: true }});
doc.addEventListener('touchstart', unlock, {{ once: true }});
}})();
</script>
""",
height=0,
)
def _inject_audio_control_sync():
"""Inject JS to sync volume and enable/disable state immediately."""
from streamlit.components.v1 import html as _html
_html(
'''
<script>
(function(){
const doc = window.parent?.document || document;
const audio = doc.getElementById('bw-bg-audio');
if (!audio) return;
// Get values from Streamlit DOM
const volInput = doc.querySelector('input[type="range"][aria-label="Volume"]');
const enableInput = doc.querySelector('input[type="checkbox"][aria-label="Enable music"]');
if (volInput) {
volInput.addEventListener('input', function(){
audio.volume = parseFloat(this.value)/100;
});
// Set initial volume
audio.volume = parseFloat(volInput.value)/100;
}
if (enableInput) {
enableInput.addEventListener('change', function(){
if (this.checked) {
audio.muted = false;
audio.play().catch(()=>{});
} else {
audio.muted = true;
audio.pause();
}
});
// Set initial mute state
if (enableInput.checked) {
audio.muted = false;
audio.play().catch(()=>{});
} else {
audio.muted = true;
audio.pause();
}
}
})();
</script>
''',
height=0,
)