initial commit
This commit is contained in:
119
app/components/CameraViewer.vue
Normal file
119
app/components/CameraViewer.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<LiveSessionPanel
|
||||
v-if="isLiveSession"
|
||||
:session="camera"
|
||||
:inline="inline"
|
||||
@close="$emit('close')"
|
||||
/>
|
||||
<aside
|
||||
v-else
|
||||
class="flex flex-col border border-kestrel-border bg-kestrel-surface"
|
||||
:class="asideClass"
|
||||
role="dialog"
|
||||
aria-label="Camera feed"
|
||||
>
|
||||
<div class="flex items-center justify-between border-b border-kestrel-border px-4 py-3 [box-shadow:0_1px_0_0_rgba(34,201,201,0.08)]">
|
||||
<h2 class="font-medium tracking-wide text-kestrel-text [text-shadow:0_0_8px_rgba(34,201,201,0.25)]">
|
||||
{{ camera?.name ?? 'Camera' }}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded p-1 text-kestrel-muted transition-colors hover:bg-kestrel-border hover:text-kestrel-accent"
|
||||
aria-label="Close panel"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<span class="text-xl leading-none">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-auto p-4">
|
||||
<div class="relative aspect-video w-full overflow-hidden rounded border border-kestrel-border bg-black [box-shadow:inset_0_0_20px_-8px_rgba(34,201,201,0.1)]">
|
||||
<template v-if="sourceType === 'hls'">
|
||||
<video
|
||||
ref="videoRef"
|
||||
class="h-full w-full object-contain"
|
||||
muted
|
||||
autoplay
|
||||
playsinline
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<img
|
||||
v-if="safeStreamUrl && !streamError"
|
||||
:src="safeStreamUrl"
|
||||
alt="Live feed"
|
||||
class="h-full w-full object-contain"
|
||||
@error="streamError = true"
|
||||
>
|
||||
<div
|
||||
v-else-if="streamError || (!safeStreamUrl && streamUrl)"
|
||||
class="flex h-full w-full items-center justify-center text-xs uppercase tracking-wider text-kestrel-muted"
|
||||
>
|
||||
[ Stream unavailable ]
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
/** Device (streamUrl, sourceType, name) or live session (id, label, hasStream) */
|
||||
camera: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
/** When true, render inline (e.g. on Cameras page) instead of overlay */
|
||||
inline: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
defineEmits(['close'])
|
||||
|
||||
const videoRef = ref(null)
|
||||
const streamError = ref(false)
|
||||
|
||||
const isLiveSession = computed(() =>
|
||||
props.camera && typeof props.camera.hasStream !== 'undefined')
|
||||
|
||||
const asideClass = computed(() =>
|
||||
props.inline ? 'rounded-lg shadow-glow' : 'absolute right-0 top-0 z-[1000] h-full w-full border-l shadow-glow md:w-[420px] [box-shadow:-8px_0_24px_-4px_rgba(34,201,201,0.12)]')
|
||||
|
||||
const streamUrl = computed(() => props.camera?.streamUrl ?? '')
|
||||
const sourceType = computed(() => (props.camera?.sourceType === 'hls' ? 'hls' : 'mjpeg'))
|
||||
|
||||
const safeStreamUrl = computed(() => {
|
||||
const u = streamUrl.value
|
||||
return typeof u === 'string' && u.trim() && (u.startsWith('http://') || u.startsWith('https://')) ? u.trim() : ''
|
||||
})
|
||||
|
||||
function initHls() {
|
||||
const url = streamUrl.value
|
||||
if (!url || sourceType.value !== 'hls' || typeof window === 'undefined') return
|
||||
const Hls = window.Hls
|
||||
if (Hls?.isSupported() && videoRef.value) {
|
||||
const hls = new Hls()
|
||||
hls.loadSource(url)
|
||||
hls.attachMedia(videoRef.value)
|
||||
}
|
||||
else if (videoRef.value?.canPlayType?.('application/vnd.apple.mpegurl')) {
|
||||
videoRef.value.src = url
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (sourceType.value === 'hls') {
|
||||
import('hls.js').then((mod) => {
|
||||
if (typeof window === 'undefined') return
|
||||
window.Hls = mod.default
|
||||
nextTick(initHls)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (videoRef.value) videoRef.value.src = ''
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user