Cameras
The Cameras.vue
component is used to display camera feed provided by the FPV camera server.
This component uses the Card, Carousel, and Skeleton components from shadcn/vue.
Features
Section titled “Features”- 4:3 aspect ratio camera views which fit the actual camera resolutions.
- Placeholder skeleton for cameras if there is no feed (aka the camera server is not running).
- Two camera layouts.
Camera Layouts
Section titled “Camera Layouts”Carousel
Section titled “Carousel”Click the small camera or thumbnail to switch places with the enlarged camera.
Camera Off Preview | Camera On Preview
Side-by-side
Section titled “Side-by-side”Click the enlarged camera to switch layouts.
Camera Off Preview | Camera On Preview
Implementation
Section titled “Implementation”Both camera layouts exist at once, but are rendered based on a v-if statement. The string reference layout.value
will store which layout is currently selected.
The FPV camera server repeatedly outputs an image, so we use an img
tag to display the camera. If the server is offline (if v-if="feed.src"
is false), a Skeleton component takes up the space where the camera feed should be.
<!-- Side-by-Side (Grid) Layout --><div class="grid-container" v-if="layout === 'grid'"> // Vue conditional for rendering side-by-side layout <div v-for="feed in cameraFeeds" :key="feed.id"> <div class="camera"> <p class="text-center text-xl font-semibold">{{ feed.name }}</p> <Card class="cursor-pointer" @click="toggleLayout"> // Click event handler for the cameras <CardContent class="flex p-0"> <Skeleton class="aspect-[4/3] w-full" v-if="!feed.src" /> // If there is no feed, the Skeleton component fills up the empty camera space <img class="image" :src="feed.src" :alt="feed.name" @error="feed.src = ''" v-if="feed.src" /> // Camera feed </CardContent> </Card> </div> </div></div>
<!-- Carousel Layout --><div class="carousel-container" v-else-if="layout === 'carousel'"> // Vue conditional for rendering carousel layout <Carousel class="p-5" @init-api="(val) => (emblaMainApi = val)" :plugins="[Fade()]"> <CarouselContent> <CarouselItem v-for="feed in cameraFeeds" :key="feed.id"> <div class="focused-camera"> <p class="text-center text-xl font-semibold">{{ feed.name }}</p> <Card class="cursor-pointer" @click="toggleLayout"> // Click event handler for the focused camera <CardContent class="flex p-0"> <Skeleton class="aspect-[4/3] w-full" v-if="!feed.src" /> // If there is no feed, the Skeleton component fills up the empty camera space <img class="image" :src="feed.src" :alt="feed.name" v-if="feed.src" /> // Camera feed </CardContent> </Card> </div> </CarouselItem> </CarouselContent> </Carousel> ...
This is the function triggered by the click event.
const toggleLayout = () => { if (layout.value === 'grid') { cleanupCarousels(); layout.value = 'carousel'; } else { layout.value = 'grid'; }};
cleanupCarousels()
simply resets the carousel/emblaAPI everytime we switch layouts, so that the camera switching functions properly.
Gallery
Section titled “Gallery”Demo Day
Section titled “Demo Day”Screenshot of MRA taking flight.
ERU camera is off, hence the static.
Carousel (Camera Off)
Section titled “Carousel (Camera Off)”Note that the actual vehicle names should be ERU and MRA.
Side-by-Side (Camera Off)
Section titled “Side-by-Side (Camera Off)”Note that the actual vehicle names should be ERU and MRA.
Carousel (Camera On)
Section titled “Carousel (Camera On)”Note that the actual vehicle names should be ERU and MRA.
Side-by-Side (Camera On)
Section titled “Side-by-Side (Camera On)”Note that the actual vehicle names should be ERU and MRA.