feat(video playback): implement picture in picture mode

This commit is contained in:
Cameron Clark
2021-04-06 12:17:04 +02:00
committed by Julien Machiels
parent 20f0929edf
commit 8c6d2ee8b1
2 changed files with 150 additions and 130 deletions
+146 -130
View File
@@ -1,148 +1,153 @@
<template>
<div>
<audio-player
v-if="isPlaying && getCurrentlyPlayingMediaType === 'Audio'"
class="d-none"
/>
<player-dialog
v-if="isPlaying && getCurrentlyPlayingMediaType === 'Video'"
dark
persistent
hide-overlay
no-click-animation
scrollable
:fullscreen="!isMinimized"
:retain-focus="!isMinimized"
:content-class="getContentClass()"
:width="$vuetify.breakpoint.mobile ? '60vw' : '25vw'"
:value="isPlaying"
>
<v-hover v-slot="{ hover }">
<v-card class="player-card" width="100%">
<video-player />
<!-- Mini Player Overlay -->
<v-fade-transition>
<v-overlay v-show="hover && isMinimized" absolute>
<div class="d-flex flex-column player-overlay">
<div class="d-flex flex-row">
<v-btn icon @click="toggleMinimized">
<v-icon>mdi-arrow-expand-all</v-icon>
</v-btn>
<v-spacer />
<v-btn icon @click="stopPlayback">
<v-icon>mdi-close</v-icon>
</v-btn>
<client-only>
<div>
<audio-player
v-if="isPlaying && getCurrentlyPlayingMediaType === 'Audio'"
class="d-none"
/>
<player-dialog
v-if="isPlaying && getCurrentlyPlayingMediaType === 'Video'"
dark
persistent
hide-overlay
no-click-animation
scrollable
:fullscreen="!isMinimized"
:retain-focus="!isMinimized"
:content-class="getContentClass()"
:width="$vuetify.breakpoint.mobile ? '60vw' : '25vw'"
:value="isPlaying"
>
<v-hover v-slot="{ hover }">
<v-card class="player-card" width="100%">
<video-player ref="videoPlayer" />
<!-- Mini Player Overlay -->
<v-fade-transition>
<v-overlay v-show="hover && isMinimized" absolute>
<div class="d-flex flex-column player-overlay">
<div class="d-flex flex-row">
<v-btn icon @click="toggleMinimized">
<v-icon>mdi-arrow-expand-all</v-icon>
</v-btn>
<v-spacer />
<v-btn icon @click="stopPlayback">
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
<div
class="absolute d-flex flex-row justify-center align-center"
>
<v-btn
class="all-pointer-events"
icon
large
@click="setPreviousTrack"
>
<v-icon size="32">mdi-skip-previous</v-icon>
</v-btn>
<v-btn
class="all-pointer-events"
icon
x-large
@click="playPause"
>
<v-icon size="48">
{{ isPaused ? 'mdi-play' : 'mdi-pause' }}
</v-icon>
</v-btn>
<v-btn
class="all-pointer-events"
icon
large
@click="setNextTrack"
>
<v-icon size="32">mdi-skip-next</v-icon>
</v-btn>
</div>
</div>
<div
class="absolute d-flex flex-row justify-center align-center"
>
<v-btn
class="all-pointer-events"
icon
large
@click="setPreviousTrack"
>
<v-icon size="32">mdi-skip-previous</v-icon>
</v-btn>
<v-btn
class="all-pointer-events"
icon
x-large
@click="playPause"
>
<v-icon size="48">
{{ isPaused ? 'mdi-play' : 'mdi-pause' }}
</v-icon>
</v-btn>
<v-btn
class="all-pointer-events"
icon
large
@click="setNextTrack"
>
<v-icon size="32">mdi-skip-next</v-icon>
</v-btn>
</div>
</div>
</v-overlay>
</v-fade-transition>
<!-- Full Screen OSD -->
<v-fade-transition>
<v-overlay v-show="!isMinimized && showFullScreenOverlay" absolute>
<div
class="d-flex flex-column justify-space-between align-center player-overlay"
</v-overlay>
</v-fade-transition>
<!-- Full Screen OSD -->
<v-fade-transition>
<v-overlay
v-show="!isMinimized && showFullScreenOverlay"
absolute
>
<div class="osd-top pt-s pl-s pr-s">
<div class="d-flex justify-space-between align-center">
<div class="d-flex">
<v-btn icon @click="stopPlayback">
<v-icon>mdi-close</v-icon>
</v-btn>
<v-btn icon @click="toggleMinimized">
<v-icon>mdi-chevron-down</v-icon>
</v-btn>
<v-btn
v-if="supportedFeatures.pictureInPicture"
icon
disabled
>
<v-icon>mdi-picture-in-picture-bottom-right</v-icon>
</v-btn>
</div>
<p class="ma-0 text-center">{{ currentItemName }}</p>
<div class="d-flex">
<v-btn icon disabled>
<v-icon>mdi-autorenew</v-icon>
</v-btn>
<v-btn v-if="supportedFeatures.airplay" icon disabled>
<v-icon>mdi-apple-airplay</v-icon>
</v-btn>
<v-btn icon disabled>
<v-icon>mdi-cast</v-icon>
</v-btn>
</div>
</div>
</div>
<div class="osd-bottom pb-s pl-s pr-s">
<div class="px-4">
<time-slider />
<div class="d-flex justify-space-between">
<div>
<v-btn icon @click="setPreviousTrack">
<v-icon>mdi-skip-previous</v-icon>
<div
class="d-flex flex-column justify-space-between align-center player-overlay"
>
<div class="osd-top pt-s pl-s pr-s">
<div class="d-flex justify-space-between align-center">
<div class="d-flex">
<v-btn icon @click="stopPlayback">
<v-icon>mdi-close</v-icon>
</v-btn>
<v-btn icon @click="playPause">
<v-icon>
{{ isPaused ? 'mdi-play' : 'mdi-pause' }}
</v-icon>
<v-btn icon @click="toggleMinimized">
<v-icon>mdi-chevron-down</v-icon>
</v-btn>
<v-btn icon @click="setNextTrack">
<v-icon icon>mdi-skip-next</v-icon>
<v-btn
v-if="supportedFeatures.pictureInPicture"
icon
@click="togglePictureInPicture"
>
<v-icon>mdi-picture-in-picture-bottom-right</v-icon>
</v-btn>
</div>
<div>
<p class="ma-0 text-center">{{ currentItemName }}</p>
<div class="d-flex">
<v-btn icon disabled>
<v-icon>mdi-closed-caption</v-icon>
<v-icon>mdi-autorenew</v-icon>
</v-btn>
<v-btn v-if="supportedFeatures.airplay" icon disabled>
<v-icon>mdi-apple-airplay</v-icon>
</v-btn>
<v-btn icon disabled>
<v-icon>mdi-cog</v-icon>
</v-btn>
<v-btn icon disabled>
<v-icon>mdi-fullscreen</v-icon>
<v-icon>mdi-cast</v-icon>
</v-btn>
</div>
</div>
</div>
<div class="osd-bottom pb-s pl-s pr-s">
<div class="px-4">
<time-slider />
<div class="d-flex justify-space-between">
<div>
<v-btn icon @click="setPreviousTrack">
<v-icon>mdi-skip-previous</v-icon>
</v-btn>
<v-btn icon @click="playPause">
<v-icon>
{{ isPaused ? 'mdi-play' : 'mdi-pause' }}
</v-icon>
</v-btn>
<v-btn icon @click="setNextTrack">
<v-icon icon>mdi-skip-next</v-icon>
</v-btn>
</div>
<div>
<v-btn icon disabled>
<v-icon>mdi-closed-caption</v-icon>
</v-btn>
<v-btn icon disabled>
<v-icon>mdi-cog</v-icon>
</v-btn>
<v-btn icon disabled>
<v-icon>mdi-fullscreen</v-icon>
</v-btn>
</div>
</div>
</div>
</div>
</div>
</div>
</v-overlay>
</v-fade-transition>
</v-card>
</v-hover>
</player-dialog>
</div>
</v-overlay>
</v-fade-transition>
</v-card>
</v-hover>
</player-dialog>
</div>
</client-only>
</template>
<script lang="ts">
@@ -572,6 +577,17 @@ export default Vue.extend({
]
});
}
},
togglePictureInPicture(): void {
// @ts-expect-error - `pictureInPictureElement` does not exist in relevant types
if (document?.pictureInPictureElement) {
// @ts-expect-error - `exitPictureInPicture` does not exist in relevant types
document.exitPictureInPicture();
// @ts-expect-error - `pictureInPictureEnabled` does not exist in relevant types
} else if (document.pictureInPictureEnabled) {
// @ts-expect-error - `togglePictureInPicture` does not exist in relevant types
this.$refs.videoPlayer.togglePictureInPicture();
}
}
}
});
@@ -247,6 +247,10 @@ export default Vue.extend({
},
onPlayerError(event: ErrorEvent): void {
this.$emit('error', event);
},
togglePictureInPicture(): void {
// @ts-expect-error - `requestPictureInPicture` does not exist in relevant types
(this.$refs.videoPlayer as HTMLVideoElement).requestPictureInPicture();
}
}
});