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/use-playerkeys';

usePlayerKeys(false);

const playbackManager = playbackManagerStore();
</script>
Expand Down
5 changes: 0 additions & 5 deletions frontend/src/components/Playback/PiPVideoPlayer.vue
Original file line number Diff line number Diff line change
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
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, noop } from '@vueuse/core';
import { playbackManagerStore, playerElementStore } from '@/store';
import { isTizen, isWebOS } from '@/utils/browser-detection';

/**
* 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(fullscreen = false, osdHandler = noop): void {
const keys = useMagicKeys();
const playbackManager = playbackManagerStore();
const playerElement = playerElementStore();

const forwardFn = (): void => {
playbackManager.skipForward();
osdHandler();
};
const backwardFn = (): void => {
playbackManager.skipBackward();
osdHandler();
};

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, forwardFn);
whenever(keys.MediaRewind, backwardFn);
whenever(keys.AudioVolumeMute, playbackManager.toggleMute);
whenever(keys.AudioVolumeUp, playbackManager.volumeUp);
whenever(keys.AudioVolumeDown, playbackManager.volumeDown);

whenever(keys.f, playerElement.toggleFullscreenPlayer);
whenever(keys.space, playbackManager.playPause);
whenever(keys.k, playbackManager.playPause);
whenever(keys.m, playbackManager.toggleMute);
whenever(keys.j, backwardFn);
whenever(keys.l, forwardFn);

if (!isTizen() && !isWebOS() && fullscreen) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to skip these platforms? I'd like to have as less platform-specific stuff as possible

Copy link
Contributor Author

@endrl endrl Jun 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While thinking about it. up,down,left,right are always used for navigation except:

  • Use left, right to seek while player is in fullscreen
    • video: the osd will become visible and should focus the slider. Now you can further seek or navigate down to the control elements and use left/right without trigger seek)
    • audio: same as video. except that the osd is always visible (visualizer mode with (probably) hidden osd is around the corner which should behave the same)

Did if forgot something? So from code view:

Concept

  • Currently v-overlay unmounts the video osd so we can't do -> mount fullscreen video + osd and focus slider
    • Attempt: listen just for left/right when video is fullscreen and osd is unmounted (to get the osd rendered, now the osd can capture focus for slider)
    • or refactor v-overlay to a div with v-show to force focus and prevent the above "temporary listener hack"

Conclusion:
Depending on the code solution the guards are required as is or modified.

Out of topic: You are immediately at the key navigation . But you don't need a TV for that. Imagine you have just a keyboard with left/top/right/down. If we get that good implemented support for all kind of TV views is ready

whenever(keys.ArrowUp, playbackManager.volumeUp);
Copy link
Contributor Author

@endrl endrl Jun 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You missed the point of onKeyStroke, you keep the btn pressed, which triggers endless events that needs to be throttled. Adds the feature "keep the button pressed to seek through audio/video or volume"

whenever(keys.ArrowDown, playbackManager.volumeDown);
whenever(keys.ArrowLeft, backwardFn);
whenever(keys.ArrowRight, forwardFn);
}
}
3 changes: 3 additions & 0 deletions frontend/src/pages/playback/music/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ import { isNil } from 'lodash-es';
import { useRoute } from 'vue-router';
import { getBlurhash } from '@/utils/images';
import { playbackManagerStore } from '@/store';
import { usePlayerKeys } from '@/composables/use-playerkeys';

const modules = [A11y, Keyboard, Virtual, EffectCoverflow];
const route = useRoute();
Expand All @@ -99,6 +100,8 @@ const coverflowEffect = {
stretch: -400
};

usePlayerKeys(true);

const backdropHash = computed(() => {
return playbackManager.currentItem
? getBlurhash(playbackManager.currentItem, ImageType.Primary)
Expand Down
10 changes: 3 additions & 7 deletions frontend/src/pages/playback/video/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ import {
useMagicKeys,
whenever
} from '@vueuse/core';
import { usePlayerKeys } from '@/composables/use-playerkeys';
import {
playbackManagerStore,
playerElementStore,
Expand Down Expand Up @@ -173,6 +174,8 @@ function handleMouseMove(): void {
timeout.start();
}

usePlayerKeys(true, handleMouseMove);

onBeforeUnmount(() => {
if (playerElement.isFullscreenVideoPlayer) {
playbackManager.stop();
Expand All @@ -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
23 changes: 20 additions & 3 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