feat: add basic media player

This commit is contained in:
Youwen Wu 2024-02-21 23:36:24 -08:00
parent 653dff41dc
commit 6cb1c9d29c
19 changed files with 344 additions and 154 deletions

View file

@ -15,7 +15,7 @@
<Dashboard />
</div>
<!-- the infotainment system -->
<div class="h-screen w-[65vw] right-0 fixed infotainment-container">
<div class="min-h-screen w-[65vw] right-0 absolute infotainment-container">
<!-- dynamic app system (edit appList.ts to add new apps) -->
<div class="mx-10 mt-10">
<svelte:component this={appList[activeApp].component} />

View file

@ -9,7 +9,6 @@ interface SongData {
artist: string
src: string
coverImg: string
slug: string
}
interface AppData {

View file

@ -5,7 +5,7 @@
<!-- Genius codeium engineering automatically loads apps -->
<div
class="flex gap-4 justify-between h-20 bg-slate-300 backdrop-blur-sm rounded-xl px-6 shadow-md"
class="app-bar backdrop-blur-xl bg-slate-300 bg-opacity-20 flex gap-4 justify-between h-20 rounded-xl px-6 shadow-md"
>
{#each Object.entries(appList) as [appName, appData]}
<button
@ -28,9 +28,9 @@
<style lang="postcss">
.app-icon {
@apply my-auto hover:brightness-50 h-16 rounded-2xl shadow-md transition-all duration-150;
@apply my-auto hover:brightness-75 h-16 rounded-2xl shadow-md transition-all duration-150;
}
.selected {
@apply brightness-50;
@apply scale-110;
}
</style>

View file

@ -3,7 +3,7 @@
</script>
<div
class="flex gap-4 w-full py-40 px-10 backdrop-blur-lg justify-center h-full camera-background rounded-3xl"
class="flex gap-4 w-full py-40 px-10 backdrop-blur-lg justify-center h-full rounded-3xl shadow-md bg-slate-300 bg-opacity-30"
>
<div class="my-auto">
<CameraContainer
@ -18,17 +18,4 @@
</div>
<style lang="postcss">
.camera-background {
background: #4b79a1; /* fallback for old browsers */
background: -webkit-linear-gradient(
to right,
#4b79a1,
#283e51
); /* Chrome 10-25, Safari 5.1-6 */
background: linear-gradient(
to right,
#4b79a1,
#283e51
); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
}
</style>

View file

@ -0,0 +1,12 @@
<script lang="ts">
import Song from './Song.svelte'
import { songList } from '../../Dashboard/MediaPlayer/songList'
</script>
<div
class="flex gap-4 w-full py-10 px-10 bg-blue-200 bg-opacity-25 backdrop-blur-xl h-full media-background rounded-3xl flex-wrap"
>
{#each Object.entries(songList) as [slug, song]}
<Song {song} {slug} />
{/each}
</div>

View file

@ -0,0 +1,50 @@
<script lang="ts">
import { musicStore } from '../../stores/musicStore'
export let song: SongData
export let slug: string
let { title, artist, coverImg } = song
const handlePlay = () => {
musicStore.setCurrent(slug)
}
const handleQueueNext = () => {
musicStore.queueNext(slug)
}
const handleQueueLast = () => {
musicStore.push(slug)
}
</script>
<div
class="flex gap-1 flex-col rounded-lg p-4 bg-slate-800 backdrop-blur-xl shadow-md w-60 flex-grow basis-1/5"
>
<img src={coverImg} alt="album cover" class="shadow-md rounded-lg w-full" />
<p class="mt-2 text-2xl font-medium">{title}</p>
<p class="text-xl text-slate-400">{artist}</p>
<div class="flex justify-center">
<div class="my-auto flex gap-4">
<button class="mt-2 hover:brightness-75" on:click={handlePlay}>
<span class="material-symbols-outlined icon fill">play_arrow</span>
</button>
<button class="mt-2 hover:brightness-75" on:click={handleQueueNext}>
<span class="material-symbols-outlined icon fill">next_plan</span>
</button>
<button class="mt-2 hover:brightness-75" on:click={handleQueueLast}>
<span class="material-symbols-outlined icon">queue_music</span>
</button>
</div>
</div>
</div>
<style lang="postcss">
.icon {
font-size: 28px;
}
.fill {
font-variation-settings: 'FILL' 1;
}
</style>

View file

@ -1,22 +0,0 @@
<script lang="ts">
import Song from './Song.svelte'
import { songList } from './songList'
</script>
<div
class="flex gap-4 w-full py-10 px-10 backdrop-blur-xl h-full media-background rounded-3xl flex-wrap"
>
{#each songList as song}
<Song {song} />
{/each}
</div>
<style lang="postcss">
.media-background {
background: radial-gradient(
circle,
rgba(238, 174, 202, 1) 0%,
rgba(148, 187, 233, 1) 100%
);
}
</style>

View file

@ -1,33 +0,0 @@
<script lang="ts">
export let song: SongData
let { title, artist, coverImg } = song
</script>
<div
class="flex gap-1 flex-col rounded-lg p-4 bg-slate-800 backdrop-blur-xl shadow-md w-60"
>
<img src={coverImg} alt="album cover" class="shadow-md rounded-lg w-full" />
<p class="mt-2 text-3xl font-medium">{title}</p>
<p class="text-xl text-slate-400">{artist}</p>
<div class="flex justify-center">
<div class="my-auto flex gap-4">
<button class="mt-2">
<span class="material-symbols-outlined icon">skip_previous</span>
</button>
<button class="mt-2">
<span class="material-symbols-outlined icon">play_arrow</span>
</button>
<button class="mt-2">
<span class="material-symbols-outlined icon">skip_next</span>
</button>
</div>
</div>
</div>
<style lang="postcss">
.icon {
font-size: 28px;
font-variation-settings: 'FILL' 1;
}
</style>

View file

@ -1,30 +0,0 @@
export const songList = [
{
title: 'Danger Zone',
artist: 'Kenny Loggins',
src: '/static/songs/danger-zone/audio.mp3',
coverImg: '/static/songs/danger-zone/cover.png',
slug: 'danger-zone',
},
{
title: 'Danger Zone',
artist: 'Kenny Loggins',
src: '/static/songs/danger-zone/audio.mp3',
coverImg: '/static/songs/danger-zone/cover.png',
slug: 'danger-zone',
},
{
title: 'Danger Zone',
artist: 'Kenny Loggins',
src: '/static/songs/danger-zone/audio.mp3',
coverImg: '/static/songs/danger-zone/cover.png',
slug: 'danger-zone',
},
{
title: 'Danger Zone',
artist: 'Kenny Loggins',
src: '/static/songs/danger-zone/audio.mp3',
coverImg: '/static/songs/danger-zone/cover.png',
slug: 'danger-zone',
},
]

View file

@ -1,5 +1,5 @@
import Camera from './Camera/Camera.svelte'
import MusicBrowser from './MusicPlayer/MusicBrowser.svelte'
import MusicBrowser from './MusicBrowser/MusicBrowser.svelte'
export const appList = {
'camera': {

View file

@ -2,9 +2,12 @@
import TopBar from './TopBar/TopBar.svelte'
import Speedometer from './Speedometer.svelte'
import SpeedLimit from './SpeedLimit.svelte'
import MediaDisplay from './MediaDisplay/MediaDisplay.svelte'
import MediaDisplay from './MediaPlayer/MediaDisplay.svelte'
import Player from './MediaPlayer/Player.svelte'
</script>
<Player />
<div class="mt-2">
<div class="px-5">
<TopBar />

View file

@ -1,28 +0,0 @@
<script lang="ts">
export let playing = false
</script>
<div class="my-auto flex gap-4">
<button class="mt-2">
<span class="material-symbols-outlined icon">skip_previous</span>
</button>
{#if playing}
<button class="mt-2">
<span class="material-symbols-outlined icon"> pause </span>
</button>
{:else}
<button class="mt-2">
<span class="material-symbols-outlined icon">play_arrow</span>
</button>
{/if}
<button class="mt-2">
<span class="material-symbols-outlined icon">skip_next</span>
</button>
</div>
<style lang="postcss">
.icon {
font-size: 34px;
font-variation-settings: 'FILL' 1;
}
</style>

View file

@ -1,20 +0,0 @@
<script lang="ts">
import Controls from './Controls.svelte'
</script>
<div class="rounded-t-lg bg-neutral-800 px-4 py-2 h-24 flex justify-between">
<div class="flex gap-6">
<div class="aspect-square">
<img
src="https://upload.wikimedia.org/wikipedia/en/thumb/2/2c/Loggins_-_Danger_Zone_single_cover.png/220px-Loggins_-_Danger_Zone_single_cover.png"
alt="album cover"
class="w-full h-full object-cover rounded-lg shadow-sm shadow-neutral-500"
/>
</div>
<div class="my-auto">
<p class="text-xl font-medium">Danger Zone</p>
<p class="text-lg text-slate-400">Kenny Loggins</p>
</div>
</div>
<Controls playing={false} />
</div>

View file

@ -0,0 +1,33 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
export let playing = false
</script>
<div class="my-auto flex gap-4 mr-4">
<button
class="mt-2 hover:brightness-75"
on:click={() => dispatch('previous')}
>
<span class="material-symbols-outlined icon">skip_previous</span>
</button>
<button class="mt-2 hover:brightness-75" on:click={() => dispatch('toggle')}>
{#if playing}
<span class="material-symbols-outlined icon">pause</span>
{:else}
<span class="material-symbols-outlined icon">play_arrow</span>
{/if}
</button>
<button class="mt-2 hover:brightness-75" on:click={() => dispatch('skip')}>
<span class="material-symbols-outlined icon">skip_next</span>
</button>
</div>
<style lang="postcss">
.icon {
font-size: 34px;
font-variation-settings: 'FILL' 1;
}
</style>

View file

@ -0,0 +1,37 @@
<script lang="ts">
import Controls from './Controls.svelte'
import { musicStore } from '../../stores/musicStore'
import { songList } from './songList'
$: currentSong = $musicStore.queue[$musicStore.currentIndex]
$: songData = songList[currentSong]
const skip = () => {
musicStore.skip()
}
const handleRewind = () => {}
const toggle = () => {
musicStore.toggle()
}
</script>
{#if songData}
<div class="rounded-t-lg bg-neutral-800 px-4 py-2 h-24 flex justify-between">
<div class="flex gap-6">
<div class="aspect-square">
<img
src={songData.coverImg}
alt="album cover"
class="w-full h-full object-cover rounded-lg shadow-sm shadow-neutral-500"
/>
</div>
<div class="my-auto">
<p class="text-xl font-medium">{songData.title}</p>
<p class="text-lg text-slate-400">{songData.artist}</p>
</div>
</div>
<Controls on:skip={skip} on:toggle={toggle} playing={$musicStore.playing} />
</div>
{/if}

View file

@ -0,0 +1,26 @@
<script lang="ts">
import { AudioManager } from './audioManager'
import { musicStore } from '../../stores/musicStore'
import { songList } from './songList'
const audioManager = new AudioManager()
$: currentSong = songList[$musicStore.queue[$musicStore.currentIndex]]
let src: string = ''
$: {
if (currentSong) src = currentSong.src
console.log(currentSong)
}
$: {
if (src !== '' && $musicStore.playing) {
audioManager.playAudio(src)
console.log(src)
} else if (!$musicStore.playing) {
console.log('stopping')
audioManager.stopAudio()
}
console.log($musicStore.queue)
console.log($musicStore.currentIndex)
}
</script>

View file

@ -0,0 +1,102 @@
export class AudioManager {
private audioContext: AudioContext | null = null
private currentSource: AudioBufferSourceNode | null = null
private currentBuffer: AudioBuffer | null = null // Stores the current audio buffer
private startTime: number = 0 // When the current playback started
private pauseTime: number = 0 // Track where we paused
private currentToken: number = 0 // Unique token for each play request
private isPaused: boolean = false
constructor() {
this.initAudioContext()
}
private async initAudioContext() {
if (!this.audioContext) {
this.audioContext = new AudioContext()
}
}
public async playAudio(url: string): Promise<void> {
if (this.isPaused && this.currentBuffer) {
// If paused, resume instead of reloading
this.resume()
return
}
this.pauseTime = 0 // Reset pause time for a new track
const playToken = ++this.currentToken // Update the token for this request
await this.initAudioContext()
if (this.audioContext) {
this.stopAudio() // Stop any currently playing audio
try {
const response = await fetch(url)
const arrayBuffer = await response.arrayBuffer()
if (this.currentToken !== playToken) {
return // Abort if a newer request has been made
}
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer)
this.currentBuffer = audioBuffer // Save the buffer for potential pausing/resuming
this.startPlayback(audioBuffer, 0) // Start playback from the beginning
} catch (error) {
console.error('Error playing audio:', error)
}
}
}
private startPlayback(buffer: AudioBuffer, offset: number) {
const source = this.audioContext!.createBufferSource()
source.buffer = buffer
source.connect(this.audioContext!.destination)
source.start(0, offset)
this.startTime = this.audioContext!.currentTime - offset
this.currentSource = source
this.isPaused = false
source.onended = () => {
if (!this.isPaused) {
this.currentBuffer = null // Clear the buffer if playback finishes normally
}
}
}
public pause() {
if (!this.isPaused && this.currentSource && this.audioContext) {
this.pauseTime = this.audioContext.currentTime - this.startTime
this.currentSource.stop()
this.isPaused = true
}
}
public resume() {
if (this.isPaused && this.currentBuffer) {
this.startPlayback(this.currentBuffer, this.pauseTime)
}
}
public stopAudio() {
if (this.currentSource) {
this.currentSource.stop()
this.currentSource = null
this.currentBuffer = null // Clear the current buffer
this.isPaused = false
this.pauseTime = 0
}
}
}
// Usage example:
const audioManager = new AudioManager()
const audioUrl = 'https://example.com/path/to/your/audio/file.mp3'
// To play the audio
audioManager.playAudio(audioUrl)
// To pause the audio
audioManager.pause()
// To resume the audio
audioManager.resume()

View file

@ -0,0 +1,14 @@
export const songList: { [key: string]: SongData } = {
'danger-zone': {
title: 'Danger Zone',
artist: 'Kenny Loggins',
src: '/static/songs/danger-zone/audio.mp3',
coverImg: '/static/songs/danger-zone/cover.png',
},
'monko-zone': {
title: 'Danger Zone',
artist: 'Kenny Loggins',
src: '/static/songs/danger-zone/audio.mp3',
coverImg: '/static/songs/danger-zone/cover.png',
},
}

View file

@ -0,0 +1,60 @@
import { writable } from 'svelte/store'
interface MusicQueue {
queue: string[]
currentIndex: number
playing: boolean
}
function createMusicStore() {
const { subscribe, set, update } = writable<MusicQueue>({
queue: [],
currentIndex: 0,
playing: false,
})
return {
subscribe,
push: (songSlug: string) =>
update(store => {
store.queue.push(songSlug)
return store
}),
skip: () =>
update(store => {
let next = store.queue[store.currentIndex + 1]
if (next !== undefined) {
store.currentIndex++
}
return store
}),
setCurrent: (songSlug: string) =>
update(store => {
store.currentIndex = store.queue.length
store.queue.push(songSlug)
return store
}),
queueNext: (songSlug: string) =>
update(store => {
store.queue.splice(store.currentIndex + 1, 0, songSlug)
return store
}),
pause: update(store => {
store.playing = false
return store
}),
play: update(store => {
store.playing = true
return store
}),
toggle: () =>
update(store => {
store.playing = !store.playing
return store
}),
reset: () => set({ queue: [], currentIndex: 0, playing: false }),
}
}
export const musicStore = createMusicStore()