fix(media): loader before peer video/audio plays + accurate error (B3/B22)
Streaming a peer file connects over mesh/Tor before the first frame, so the
player sat blank. Add a loading state:
- PeerFiles video modal: spinner overlay ("Connecting to peer…") until the
<video> fires playing/canplay; an error overlay on failure instead of a
silent black box.
- useAudioPlayer: loading flag driven by loadstart/waiting vs canplay/playing;
GlobalAudioPlayer shows a spinner in the transport button while connecting.
- Fix the misleading audio error "Could not play audio. File Browser may not be
running." (wrong for peer content) → "Could not play this audio file. The peer
may be offline…" (B22).
type-check clean; useAudioPlayer tests 10/10.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
921363542c
commit
c481afc7d9
@ -25,7 +25,11 @@
|
|||||||
class="flex-shrink-0 w-9 h-9 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center transition-colors"
|
class="flex-shrink-0 w-9 h-9 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center transition-colors"
|
||||||
@click="togglePlay"
|
@click="togglePlay"
|
||||||
>
|
>
|
||||||
<svg v-if="!audioPlayer.playing.value" class="w-5 h-5 text-white ml-0.5" fill="currentColor" viewBox="0 0 24 24">
|
<svg v-if="audioPlayer.loading.value" class="w-5 h-5 animate-spin text-white" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.4 0 0 5.4 0 12h4z" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else-if="!audioPlayer.playing.value" class="w-5 h-5 text-white ml-0.5" fill="currentColor" viewBox="0 0 24 24">
|
||||||
<path d="M8 5v14l11-7L8 5z" />
|
<path d="M8 5v14l11-7L8 5z" />
|
||||||
</svg>
|
</svg>
|
||||||
<svg v-else class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
|
<svg v-else class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
|||||||
@ -4,6 +4,7 @@ const audio = ref<HTMLAudioElement | null>(null)
|
|||||||
const currentSrc = ref<string | null>(null)
|
const currentSrc = ref<string | null>(null)
|
||||||
const currentName = ref('')
|
const currentName = ref('')
|
||||||
const playing = ref(false)
|
const playing = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
const currentTime = ref(0)
|
const currentTime = ref(0)
|
||||||
const duration = ref(0)
|
const duration = ref(0)
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
@ -21,8 +22,22 @@ function init() {
|
|||||||
duration.value = audio.value?.duration ?? 0
|
duration.value = audio.value?.duration ?? 0
|
||||||
error.value = null
|
error.value = null
|
||||||
})
|
})
|
||||||
|
// Buffering / connecting over mesh|Tor → show a loader until it can play.
|
||||||
|
audio.value.addEventListener('loadstart', () => {
|
||||||
|
loading.value = true
|
||||||
|
})
|
||||||
|
audio.value.addEventListener('waiting', () => {
|
||||||
|
loading.value = true
|
||||||
|
})
|
||||||
|
audio.value.addEventListener('canplay', () => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
audio.value.addEventListener('playing', () => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
audio.value.addEventListener('ended', () => {
|
audio.value.addEventListener('ended', () => {
|
||||||
playing.value = false
|
playing.value = false
|
||||||
|
loading.value = false
|
||||||
})
|
})
|
||||||
audio.value.addEventListener('pause', () => {
|
audio.value.addEventListener('pause', () => {
|
||||||
playing.value = false
|
playing.value = false
|
||||||
@ -33,7 +48,8 @@ function init() {
|
|||||||
})
|
})
|
||||||
audio.value.addEventListener('error', () => {
|
audio.value.addEventListener('error', () => {
|
||||||
playing.value = false
|
playing.value = false
|
||||||
error.value = 'Could not play audio. File Browser may not be running.'
|
loading.value = false
|
||||||
|
error.value = 'Could not play this audio file. The peer may be offline, or the file may be unavailable.'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,6 +63,7 @@ function play(src: string, name: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (currentSrc.value !== src) {
|
if (currentSrc.value !== src) {
|
||||||
|
loading.value = true
|
||||||
audio.value!.src = src
|
audio.value!.src = src
|
||||||
currentSrc.value = src
|
currentSrc.value = src
|
||||||
currentName.value = name
|
currentName.value = name
|
||||||
@ -87,6 +104,7 @@ export function useAudioPlayer() {
|
|||||||
seek,
|
seek,
|
||||||
stop,
|
stop,
|
||||||
playing,
|
playing,
|
||||||
|
loading,
|
||||||
currentName,
|
currentName,
|
||||||
currentTime,
|
currentTime,
|
||||||
duration,
|
duration,
|
||||||
|
|||||||
@ -234,12 +234,30 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<!-- Video element -->
|
<!-- Video element -->
|
||||||
<video
|
<div class="relative">
|
||||||
:src="videoPlayerUrl"
|
<video
|
||||||
class="w-full rounded-xl"
|
:src="videoPlayerUrl"
|
||||||
controls
|
class="w-full rounded-xl bg-black"
|
||||||
autoplay
|
controls
|
||||||
/>
|
autoplay
|
||||||
|
@playing="videoLoading = false"
|
||||||
|
@canplay="videoLoading = false"
|
||||||
|
@error="videoLoading = false; videoError = true"
|
||||||
|
/>
|
||||||
|
<!-- Loader while the stream connects over mesh/Tor -->
|
||||||
|
<div v-if="videoLoading && !videoError" class="absolute inset-0 flex flex-col items-center justify-center gap-3 rounded-xl bg-black/60 pointer-events-none">
|
||||||
|
<svg class="w-8 h-8 animate-spin text-white/80" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.4 0 0 5.4 0 12h4z" />
|
||||||
|
</svg>
|
||||||
|
<span class="text-sm text-white/70">Connecting to peer…</span>
|
||||||
|
</div>
|
||||||
|
<!-- Error state -->
|
||||||
|
<div v-if="videoError" class="absolute inset-0 flex flex-col items-center justify-center gap-2 rounded-xl bg-black/70 text-center px-4">
|
||||||
|
<p class="text-sm text-white/80">Couldn't play this video</p>
|
||||||
|
<p class="text-xs text-white/50">The peer may be offline, or this preview can't be played. Try downloading it instead.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Info bar -->
|
<!-- Info bar -->
|
||||||
<div class="mt-3 flex items-center justify-between">
|
<div class="mt-3 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
@ -320,6 +338,10 @@ const audioPlayer = useAudioPlayer()
|
|||||||
const videoPlayerItem = ref<CatalogItem | null>(null)
|
const videoPlayerItem = ref<CatalogItem | null>(null)
|
||||||
const videoPlayerUrl = ref<string | null>(null)
|
const videoPlayerUrl = ref<string | null>(null)
|
||||||
const videoPlayerPaid = ref(false)
|
const videoPlayerPaid = ref(false)
|
||||||
|
// Streaming a peer's file connects over mesh/Tor before the first frame, so
|
||||||
|
// show a loader until the element can actually play (or errors).
|
||||||
|
const videoLoading = ref(false)
|
||||||
|
const videoError = ref(false)
|
||||||
|
|
||||||
const peerDisplayName = computed(() => {
|
const peerDisplayName = computed(() => {
|
||||||
if (currentPeer.value?.name) return currentPeer.value.name
|
if (currentPeer.value?.name) return currentPeer.value.name
|
||||||
@ -604,8 +626,19 @@ function closeVideoPlayer() {
|
|||||||
videoPlayerItem.value = null
|
videoPlayerItem.value = null
|
||||||
videoPlayerUrl.value = null
|
videoPlayerUrl.value = null
|
||||||
videoPlayerPaid.value = false
|
videoPlayerPaid.value = false
|
||||||
|
videoLoading.value = false
|
||||||
|
videoError.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show the loader the moment a video opens; the element's playing/canplay/error
|
||||||
|
// events clear it.
|
||||||
|
watch(videoPlayerUrl, (url) => {
|
||||||
|
if (url) {
|
||||||
|
videoLoading.value = true
|
||||||
|
videoError.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
function triggerDownload(base64Data: string, item: CatalogItem) {
|
function triggerDownload(base64Data: string, item: CatalogItem) {
|
||||||
const blob = new Blob(
|
const blob = new Blob(
|
||||||
[Uint8Array.from(atob(base64Data), c => c.charCodeAt(0))],
|
[Uint8Array.from(atob(base64Data), c => c.charCodeAt(0))],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user