Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(player): Add media key control support #2001

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
3 changes: 3 additions & 0 deletions frontend/src/components/Layout/AudioControls.vue
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@
<script setup lang="ts">
import { getItemDetailsLink } from '@/utils/items';
import { playbackManagerStore } from '@/store';
import { usePlayerKeys } from '@/composables';

usePlayerKeys();

const playbackManager = playbackManagerStore();
</script>
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/components/Layout/TimeSlider.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
:max="runtime"
thumb-label
validate-on="input"
:focused="focusedTimeSlider"
@start="clicked = true"
@end="onRelease">
@end="onRelease"
@blur="focusedTimeSlider = false">
<template #prepend>
{{ formatTime(playbackManager.currentTime) }}
</template>
Expand All @@ -23,6 +25,7 @@
import { computed, ref } from 'vue';
import { playbackManagerStore } from '@/store';
import { ticksToMs, formatTime } from '@/utils/time';
import { focusedTimeSlider } from '@/composables/use-playerkeys';

const playbackManager = playbackManagerStore();
const currentInput = ref(0);
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/components/Layout/VolumeSlider.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
class="volume-slider"
hide-details
thumb-label
max="100">
max="100"
:focused="focusedVolumeSlider"
@blur="focusedVolumeSlider = false">
<template #thumb-label>
{{ Math.round(sliderValue) }}
</template>
Expand All @@ -27,6 +29,7 @@ import IMdiVolumeMedium from 'virtual:icons/mdi/volume-medium';
import IMdiVolumeHigh from 'virtual:icons/mdi/volume-high';
import IMdiVolumeLow from 'virtual:icons/mdi/volume-low';
import { playbackManagerStore } from '@/store';
import { focusedVolumeSlider } from '@/composables/use-playerkeys';

const playbackManager = playbackManagerStore();

Expand Down
7 changes: 1 addition & 6 deletions frontend/src/components/Playback/PiPVideoPlayer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
width="100%">
<div class="d-flex flex-column">
<div class="d-flex flex-row">
<v-btn icon @click="playerElement.toggleFullscreenVideoPlayer">
<v-btn icon @click="playerElement.toggleFullscreenPlayer">
<v-icon>
<i-mdi-arrow-expand-all />
</v-icon>
Expand Down Expand Up @@ -71,16 +71,11 @@

<script setup lang="ts">
import { onMounted, onBeforeUnmount } from 'vue';
import { useMagicKeys, whenever } from '@vueuse/core';
import { playbackManagerStore, playerElementStore } from '@/store';

const playerElement = playerElementStore();
const playbackManager = playbackManagerStore();

const keys = useMagicKeys();

whenever(keys.f, playerElement.toggleFullscreenVideoPlayer);

onMounted(() => {
playerElement.isPiPMounted = true;
});
Expand Down
1 change: 1 addition & 0 deletions frontend/src/composables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export { useRemote } from './use-remote';
export { useVuetify } from './use-vuetify';
export { useResponsiveClasses } from './use-responsive-classes';
export { useDateFns } from './use-datefns';
export { usePlayerKeys } from './use-playerkeys';
export { useRouter } from './use-router';
/**
* == COMPONENT COMPOSABLES ==
Expand Down
51 changes: 51 additions & 0 deletions frontend/src/composables/use-playerkeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { whenever, useMagicKeys, refAutoReset } from '@vueuse/core';
import { playbackManagerStore, playerElementStore } from '@/store';

export const focusedTimeSlider = refAutoReset(false, 1000);
export const focusedVolumeSlider = refAutoReset(false, 1000);

/**
* Register keyboard player control.
* @param fullscreen - Arrow key updates when playback is happening outside of fullscreen are ignored
* @param osdHandler - show the player osd for a short period whenever a suitable action is called
*/
export function usePlayerKeys(): void {
const keys = useMagicKeys();
const playbackManager = playbackManagerStore();
const playerElement = playerElementStore();

whenever(keys.MediaPause, playbackManager.pause);
whenever(keys.Pause, playbackManager.pause);
whenever(keys.MediaPlay, playbackManager.unpause);
whenever(keys.MediaPlayPause, playbackManager.playPause);
whenever(keys.MediaStop, playbackManager.stop);
whenever(keys.Exit, playbackManager.stop);
whenever(keys.MediaTrackNext, playbackManager.setNextTrack);
whenever(keys.MediaTrackPrevious, playbackManager.setPreviousTrack);
whenever(keys.MediaFastForward, () => (focusedTimeSlider.value = true));
whenever(keys.MediaRewind, () => (focusedTimeSlider.value = true));
whenever(keys.AudioVolumeMute, playbackManager.toggleMute);
whenever(keys.AudioVolumeUp, () => (focusedVolumeSlider.value = true));
whenever(keys.AudioVolumeDown, () => (focusedVolumeSlider.value = true));

whenever(keys.space, playbackManager.playPause);
whenever(keys.k, playbackManager.playPause);
whenever(keys.m, playbackManager.toggleMute);
whenever(keys.j, () => (focusedTimeSlider.value = true));
whenever(keys.l, () => (focusedTimeSlider.value = true));

/**
* This key conflicts with the browser's fullscreen function in the
* fullscreen video player, hence why we skip it
*/
if (!playerElement.isFullscreenVideoPlayer) {
whenever(keys.f, playerElement.toggleFullscreenPlayer);
}

if (playerElement.isFullscreenPlayer) {
whenever(keys.ArrowUp, () => (focusedVolumeSlider.value = true));
whenever(keys.ArrowDown, () => (focusedVolumeSlider.value = true));
whenever(keys.ArrowLeft, () => (focusedTimeSlider.value = true));
whenever(keys.ArrowRight, () => (focusedTimeSlider.value = true));
}
}
9 changes: 5 additions & 4 deletions frontend/src/pages/playback/music/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
:autoplay="false"
effect="coverflow"
:coverflow-effect="coverflowEffect"
keyboard
a11y
virtual
@slide-change="onSlideChange"
Expand Down Expand Up @@ -75,20 +74,20 @@ meta:
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { ImageType } from '@jellyfin/sdk/lib/generated-client';
import { A11y, Keyboard, Virtual, EffectCoverflow } from 'swiper';
import { A11y, Virtual, EffectCoverflow } from 'swiper';
import type SwiperType from 'swiper';
import 'swiper/css';
import 'swiper/css/a11y';
import 'swiper/css/keyboard';
import 'swiper/css/effect-coverflow';
import 'swiper/css/virtual';
import { Swiper, SwiperSlide } from 'swiper/vue';
import { isNil } from 'lodash-es';
import { useRoute } from 'vue-router';
import { getBlurhash } from '@/utils/images';
import { playbackManagerStore } from '@/store';
import { usePlayerKeys } from '@/composables';

const modules = [A11y, Keyboard, Virtual, EffectCoverflow];
const modules = [A11y, Virtual, EffectCoverflow];
const route = useRoute();

const playbackManager = playbackManagerStore();
Expand All @@ -99,6 +98,8 @@ const coverflowEffect = {
stretch: -400
};

usePlayerKeys();

const backdropHash = computed(() => {
return playbackManager.currentItem
? getBlurhash(playbackManager.currentItem, ImageType.Primary)
Expand Down
12 changes: 4 additions & 8 deletions frontend/src/pages/playback/video/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<v-btn :icon="IMdiClose" @click="playbackManager.stop" />
<v-btn
:icon="IMdiChevronDown"
@click="playerElement.toggleFullscreenVideoPlayer" />
@click="playerElement.toggleFullscreenPlayer" />
</div>
<div class="d-flex ml-auto">
<cast-button />
Expand Down Expand Up @@ -129,6 +129,7 @@ import {
useMagicKeys,
whenever
} from '@vueuse/core';
import { usePlayerKeys } from '@/composables';
import {
playbackManagerStore,
playerElementStore,
Expand All @@ -137,6 +138,8 @@ import {
} from '@/store';
import { getEndsAtTime } from '@/utils/time';

usePlayerKeys();

/**
* - iOS's Safari fullscreen API is only available for the video element
*/
Expand Down Expand Up @@ -189,14 +192,7 @@ onMounted(() => {
playerElement.isFullscreenMounted = true;
});

whenever(keys.space, playbackManager.playPause);
whenever(keys.k, playbackManager.playPause);
whenever(keys.right, playbackManager.skipForward);
whenever(keys.l, playbackManager.skipForward);
whenever(keys.left, playbackManager.skipBackward);
whenever(keys.j, playbackManager.skipBackward);
whenever(keys.f, fullscreen.toggle);
whenever(keys.m, playbackManager.toggleMute);

watch(staticOverlay, (val) => {
if (val) {
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/store/playbackManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,20 @@ class PlaybackManagerStore {
this.isMuted = !this.isMuted;
};

/**
* Increase volume by 5
*/
public volumeUp = (): void => {
this.currentVolume = this.currentVolume + 5;
};

/**
* Decrease volume by 5
*/
public volumeDown = (): void => {
this.currentVolume = this.currentVolume - 5;
};

public instantMixFromItem = async (itemId: string): Promise<void> => {
const items = (
await remote.sdk.newUserApi(getInstantMixApi).getInstantMixFromItem({
Expand Down
25 changes: 21 additions & 4 deletions frontend/src/store/playerElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,33 @@ class PlayerElementStore {
return useRouter().currentRoute.value.fullPath === fullscreenVideoRoute;
}

public get isFullscreenMusicPlayer(): boolean {
return useRouter().currentRoute.value.fullPath === fullscreenMusicRoute;
}

public get isFullscreenPlayer(): boolean {
return this.isFullscreenMusicPlayer || this.isFullscreenVideoPlayer;
}

/**
* == ACTIONS ==
*/
public toggleFullscreenVideoPlayer = async (): Promise<void> => {
public toggleFullscreenPlayer = async (): Promise<void> => {
const router = useRouter();

if (this.isFullscreenVideoPlayer) {
if (this.isFullscreenPlayer) {
router.back();
} else {
await router.push(fullscreenVideoRoute);
switch (playbackManager.currentlyPlayingMediaType) {
case 'Video': {
await router.push(fullscreenVideoRoute);
break;
}
case 'Audio': {
await router.push(fullscreenMusicRoute);
break;
}
}
}
};

Expand Down Expand Up @@ -226,7 +243,7 @@ class PlayerElementStore {
!oldValue &&
playbackManager.currentlyPlayingMediaType === 'Video'
) {
await this.toggleFullscreenVideoPlayer();
await this.toggleFullscreenPlayer();
}
}
);
Expand Down