Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/helpers/player_menu_items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,27 @@ export const getPlayerMenuItems = (
),
});
}
// add playback speed submenu
if (playerQueue) {
const currentSpeed =
typeof playerQueue.extra_attributes?.playback_speed === "number"
? (playerQueue.extra_attributes.playback_speed as number)
: 1.0;
menuItems.push({
label: "playback_speed",
labelArgs: [],
icon: "mdi-speedometer",
subItems: [0.5, 0.75, 1.0, 1.25, 1.5, 2.0].map((speed) => ({
label: speed === 1.0 ? "1×" : `${speed}×`,
labelArgs: [],
selected: Math.abs(currentSpeed - speed) < 0.01,
action: () => {
api.queueCommandSetPlaybackSpeed(playerQueue!.queue_id, speed);
},
})),
});
}

// add 'clear queue' menu item
if (playerQueue?.items) {
menuItems.push({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<template>
<v-menu
v-if="isVisible && playerQueue"
location="top"
:close-on-content-click="true"
>
<template #activator="{ props: menu }">
<Button
v-bind="{ ...menu }"
variant="icon"
:title="$t('playback_speed')"
:disabled="!playerQueue.active || playerQueue.items === 0"
:style="currentSpeed !== 1.0 ? { color: activeColor } : {}"
>
<span class="speed-label">{{ formatSpeed(currentSpeed) }}</span>
</Button>
</template>

<v-list density="compact" nav>
<v-list-subheader>{{ $t("playback_speed") }}</v-list-subheader>
<v-list-item
v-for="speed in SPEED_PRESETS"
:key="speed"
:title="formatSpeed(speed)"
:active="Math.abs(currentSpeed - speed) < 0.01"
active-color="primary"
@click="setSpeed(speed)"
/>
</v-list>
</v-menu>
</template>

<script setup lang="ts">
import Button from "@/components/Button.vue";
import api from "@/plugins/api";
import { PlayerQueue } from "@/plugins/api/interfaces";
import { computed } from "vue";

const SPEED_PRESETS = [0.5, 0.75, 1.0, 1.25, 1.5, 2.0];

// properties
export interface Props {
playerQueue: PlayerQueue | undefined;
isVisible?: boolean;
activeColor?: string;
}

const props = withDefaults(defineProps<Props>(), {
isVisible: true,
activeColor: "rgb(var(--v-theme-primary))",
});

const currentSpeed = computed<number>(() => {
const speed = props.playerQueue?.extra_attributes?.playback_speed;
Copy link
Member

Choose a reason for hiding this comment

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

so the playback_speed will now be set on the (current) queue item instead and not at queue level

Copy link
Member

Choose a reason for hiding this comment

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

See here for details:
music-assistant/server@90f369d

return typeof speed === "number" ? speed : 1.0;
});

function formatSpeed(speed: number): string {
return speed === 1.0 ? "1×" : `${speed}×`;
}

function setSpeed(speed: number) {
if (props.playerQueue) {
api.queueCommandSetPlaybackSpeed(props.playerQueue.queue_id, speed);
}
}
</script>

<style scoped>
.speed-label {
font-size: 0.75rem;
font-weight: 600;
min-width: 28px;
text-align: center;
}
</style>
16 changes: 16 additions & 0 deletions src/layouts/default/PlayerOSD/PlayerFullscreen.vue
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,12 @@
class="media-controls-item"
max-height="35px"
/>
<PlaybackSpeedBtn
:player-queue="store.activePlayerQueue"
:active-color="sliderColor"
:is-visible="isSpeechContent"
class="media-controls-item"
/>
<QueueBtn
v-if="store.activePlayerQueue"
class="media-controls-item"
Expand Down Expand Up @@ -534,6 +540,7 @@ import {
import NextBtn from "@/layouts/default/PlayerOSD/PlayerControlBtn/NextBtn.vue";
import PlayBtn from "@/layouts/default/PlayerOSD/PlayerControlBtn/PlayBtn.vue";
import PreviousBtn from "@/layouts/default/PlayerOSD/PlayerControlBtn/PreviousBtn.vue";
import PlaybackSpeedBtn from "@/layouts/default/PlayerOSD/PlayerControlBtn/PlaybackSpeedBtn.vue";
import RepeatBtn from "@/layouts/default/PlayerOSD/PlayerControlBtn/RepeatBtn.vue";
import ShuffleBtn from "@/layouts/default/PlayerOSD/PlayerControlBtn/ShuffleBtn.vue";
import PlayerVolume from "@/layouts/default/PlayerOSD/PlayerVolume.vue";
Expand Down Expand Up @@ -769,6 +776,15 @@ const showExpandedPlayerSelectButton = computed(() => {
return vuetify.display.height.value > 800;
});

const isSpeechContent = computed(() => {
const mediaType = store.activePlayerQueue?.current_item?.media_item?.media_type;
return (
mediaType === MediaType.PODCAST ||
mediaType === MediaType.PODCAST_EPISODE ||
mediaType === MediaType.AUDIOBOOK
);
});

// methods

const itemClick = function (item: MediaItemType) {
Expand Down
4 changes: 4 additions & 0 deletions src/plugins/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1203,6 +1203,10 @@ export class MusicAssistantApi {
!this.queues[queueId].dont_stop_the_music_enabled,
);
}
public queueCommandSetPlaybackSpeed(queueId: string, speed: number) {
// Set the playback speed for the queue (1.0 = normal, 1.5 = 1.5x, etc.)
this.playerQueueCommand(queueId, "set_playback_speed", { speed });
}
public playerQueueCommand(
queue_id: string,
command: string,
Expand Down
1 change: 1 addition & 0 deletions src/plugins/api/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,7 @@ export interface PlayerQueue {
current_item?: QueueItem;
next_item?: QueueItem;
radio_source: MediaItemType[];
extra_attributes?: Record<string, string | number | boolean>;
}

// player
Expand Down
1 change: 1 addition & 0 deletions src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,7 @@
},
"show_info": "Show info",
"show_select_boxes": "Show selection boxes",
"playback_speed": "Playback speed",
"shuffle": "Shuffle",
"sort": {
"duration": "Duration",
Expand Down
Loading