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

client: refactor media controls to use composables instead of hacky component refs #1572

Merged
merged 24 commits into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0f43f56
refactor basic media controls into useMediaControls
dyc3 Mar 25, 2024
316bd99
refactor captions controls into useCaptions
dyc3 Mar 25, 2024
29ae8d4
refactor playback rate controls into usePlaybackRate
dyc3 Mar 26, 2024
b3b9ef7
simplify how playback rate controls are hooked up
dyc3 Mar 26, 2024
219a258
simplify how captions controls are hooked up
dyc3 Mar 26, 2024
8cafe85
fix some errors being emitted and remove dead code
dyc3 Mar 26, 2024
a95af6d
fix youtube and plyr players not grabbing captions when they can
dyc3 Mar 26, 2024
2d58daa
fix playback rate not getting applied on room join
dyc3 Mar 26, 2024
e1af77f
fix errors emitted by not properly waiting for player apis to be ready
dyc3 Mar 26, 2024
a2bb44e
fix misc other bugs
dyc3 Mar 26, 2024
2302475
better logging around waitForPlayer
dyc3 Mar 26, 2024
3708508
fix mediaplayerv2 using nested refs and not completely working right
dyc3 Mar 26, 2024
12b5c62
fix plyr player not respecting play/pause and seeks
dyc3 Mar 26, 2024
806c0c4
correctly order player switch and apiready emit
dyc3 Mar 26, 2024
2ab1356
move MediaPlayer types to fix lint
dyc3 Mar 27, 2024
1738635
update playback rate switcher unit tests
dyc3 Mar 27, 2024
0cb50be
track `hasPlayerChangedYet`
dyc3 Mar 28, 2024
5672401
fix playback rate not updating properly
dyc3 Mar 28, 2024
18bda67
comment out some code
dyc3 Mar 28, 2024
1700014
fix closed captions button possibly not activating when it should on …
dyc3 Mar 28, 2024
09dd020
add comment
dyc3 Mar 28, 2024
c936ffa
refactor playback rate supported data connection to ui
dyc3 Mar 28, 2024
592a4d7
alias captions supported ref so that it actually works
dyc3 Mar 28, 2024
c126f90
fix apiready getting triggered too early on player swap
dyc3 Mar 28, 2024
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
140 changes: 139 additions & 1 deletion client/src/components/composables/media-player.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useStore } from "@/store";
import { onMounted, ref, watch } from "vue";
import { onMounted, ref, watch, type Ref, shallowRef, provide, inject, computed } from "vue";

const volume = ref(100);

Expand All @@ -16,3 +16,141 @@

return volume;
}

export interface MediaPlayer {
/**
* Play the video.
*
* Some browsers emit promises for this, and some don't.
*/
play(): void | Promise<void>;
/**
* Pause the video.
*
* Some browsers emit promises for this, and some don't.
*/
pause(): void | Promise<void>;
setVolume(volume: number): void | Promise<void>;
getPosition(): number;
setPosition(position: number): void;

isCaptionsSupported(): boolean;
getAvailablePlaybackRates(): number[];
}

export interface MediaPlayerWithCaptions extends MediaPlayer {
isCaptionsEnabled(): boolean;
setCaptionsEnabled(enabled: boolean): void;
getCaptionsTracks(): string[];
setCaptionsTrack(track: string): void;
}

export interface MediaPlayerWithPlaybackRate extends MediaPlayer {
getPlaybackRate(): number;
setPlaybackRate(rate: number): void;
}

export class MediaPlayerV2 {
player: Ref<MediaPlayer | null> = shallowRef(null);
apiReady = ref(false);
playing = ref(false);
isCaptionsSupported = ref(false);

setPlayer(player: MediaPlayer | null) {
this.apiReady.value = false;
this.player.value = player;
}

Check warning on line 62 in client/src/components/composables/media-player.ts

View check run for this annotation

Codecov / codecov/patch

client/src/components/composables/media-player.ts#L60-L62

Added lines #L60 - L62 were not covered by tests

checkForPlayer(p: MediaPlayer | null): p is MediaPlayer {
if (!p) {
return false;
}
return this.apiReady.value ?? false;
}

Check warning on line 69 in client/src/components/composables/media-player.ts

View check run for this annotation

Codecov / codecov/patch

client/src/components/composables/media-player.ts#L65-L69

Added lines #L65 - L69 were not covered by tests

isPlayerPresent(): boolean {
return !!this.player.value;
}

Check warning on line 73 in client/src/components/composables/media-player.ts

View check run for this annotation

Codecov / codecov/patch

client/src/components/composables/media-player.ts#L72-L73

Added lines #L72 - L73 were not covered by tests

markApiReady() {
if (!this.player.value) {
// FIXME: im not sure if this branch gets taken anymore
new Promise(resolve => {
const stop = watch(this.player, newPlayer => {
if (newPlayer) {
stop();
resolve(true);
}
});
}).then(() => {
this.apiReady.value = true;
});
} else {
this.apiReady.value = true;
}
}

Check warning on line 91 in client/src/components/composables/media-player.ts

View check run for this annotation

Codecov / codecov/patch

client/src/components/composables/media-player.ts#L76-L91

Added lines #L76 - L91 were not covered by tests

async play(): Promise<void> {
if (!this.checkForPlayer(this.player.value)) {
return Promise.reject("Player not available yet");
}
return this.player.value.play();
}

Check warning on line 98 in client/src/components/composables/media-player.ts

View check run for this annotation

Codecov / codecov/patch

client/src/components/composables/media-player.ts#L94-L98

Added lines #L94 - L98 were not covered by tests
async pause(): Promise<void> {
if (!this.checkForPlayer(this.player.value)) {
return Promise.reject("Player not available yet");
}
return this.player.value.pause();
}

Check warning on line 104 in client/src/components/composables/media-player.ts

View check run for this annotation

Codecov / codecov/patch

client/src/components/composables/media-player.ts#L100-L104

Added lines #L100 - L104 were not covered by tests
getPosition(): number {
if (!this.checkForPlayer(this.player.value)) {
return 0;
}
return this.player.value.getPosition();
}

Check warning on line 110 in client/src/components/composables/media-player.ts

View check run for this annotation

Codecov / codecov/patch

client/src/components/composables/media-player.ts#L106-L110

Added lines #L106 - L110 were not covered by tests
setPosition(position: number): void {
if (!this.checkForPlayer(this.player.value)) {
return;
}
return this.player.value.setPosition(position);
}

Check warning on line 116 in client/src/components/composables/media-player.ts

View check run for this annotation

Codecov / codecov/patch

client/src/components/composables/media-player.ts#L112-L116

Added lines #L112 - L116 were not covered by tests
}

const PLAYER_KEY = Symbol("player");
const player = new MediaPlayerV2();

export function useMediaPlayer() {
return inject(PLAYER_KEY, player);
}

Check warning on line 124 in client/src/components/composables/media-player.ts

View check run for this annotation

Codecov / codecov/patch

client/src/components/composables/media-player.ts#L123-L124

Added lines #L123 - L124 were not covered by tests

// export function isPlayerPresent(p: typeof player): p is Ref<MediaPlayerV2> {
// return !!p.value;
// }

const isCaptionsSupported: Ref<boolean> = ref(false);
const isCaptionsEnabled: Ref<boolean> = ref(false);
const captionsTracks: Ref<string[]> = ref([]);
const currentTrack: Ref<string | null> = ref(null);

export function useCaptions() {
return {
isCaptionsSupported,
isCaptionsEnabled,
captionsTracks,
currentTrack,
};
}

const playbackRate: Ref<number> = ref(1);
const availablePlaybackRates: Ref<number[]> = ref([1]);
const isPlaybackRateSupported = computed(() => {
return availablePlaybackRates.value.length > 1;
});

export function usePlaybackRate() {
return {
isPlaybackRateSupported,
playbackRate,
availablePlaybackRates,
};
}
44 changes: 19 additions & 25 deletions client/src/components/controls/ClosedCaptionsSwitcher.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@
<v-icon>mdi-closed-caption</v-icon>
<v-menu location="top" offset-y activator="parent" :disabled="!supported">
<v-list>
<v-list-item link @click="setCaptionsEnabled(true)" v-if="tracks.length === 0">
<v-list-item
link
@click="setCaptionsEnabled(true)"
v-if="captions.captionsTracks.value.length === 0"
>
On
</v-list-item>
<v-list-item
link
@click="setCaptionsTrack(track)"
v-for="(track, idx) in tracks"
v-for="(track, idx) in captions.captionsTracks.value"
:key="idx"
>
{{ track }}
Expand All @@ -26,33 +30,23 @@
</v-btn>
</template>

<script lang="ts">
import { defineComponent, PropType } from "vue";
<script lang="ts" setup>
import { useCaptions } from "../composables";

const ClosedCaptionsSwitcher = defineComponent({
name: "ClosedCaptionsSwitcher",
emits: ["enable-cc", "cc-track"],
props: {
supported: { type: Boolean, default: true },
tracks: { type: Array as PropType<string[]>, default: () => [] },
},
setup(props, { emit }) {
function setCaptionsEnabled(value: boolean) {
emit("enable-cc", value);
}
const captions = useCaptions();

function setCaptionsTrack(value: string) {
emit("cc-track", value);
}
function setCaptionsEnabled(value: boolean) {
captions.isCaptionsEnabled.value = value;
}

return {
setCaptionsEnabled,
setCaptionsTrack,
};
},
});
function setCaptionsTrack(value: string) {
if (!captions.isCaptionsEnabled.value) {
captions.isCaptionsEnabled.value = true;
}
captions.currentTrack.value = value;
}

export default ClosedCaptionsSwitcher;
const supported = captions.isCaptionsSupported;
</script>

<style lang="scss">
Expand Down
66 changes: 19 additions & 47 deletions client/src/components/controls/PlaybackRateSwitcher.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<template>
<v-btn variant="text" class="media-control" aria-label="Playback Speed" :disabled="!supported">
{{ formatRate(currentRate) }}
{{ formatRate(playbackRate.playbackRate.value) }}

<v-menu location="top" activator="parent">
<v-list>
<v-list-item
v-for="(rate, index) in availableRates"
v-for="(rate, index) in playbackRate.availablePlaybackRates.value"
:key="index"
:value="index"
:value="rate"
@click="setRate(rate)"
>
<v-list-item-title>{{ formatRate(rate) }}</v-list-item-title>
Expand All @@ -17,56 +17,28 @@
</v-btn>
</template>

<script lang="ts">
import { defineComponent, PropType, computed } from "vue";
<script lang="ts" setup>
import { useConnection } from "@/plugins/connection";
import { useRoomApi } from "@/util/roomapi";
import { usePlaybackRate } from "../composables";

const PlaybackRateSwitcher = defineComponent({
name: "PlaybackRateSwitcher",
props: {
currentRate: {
type: Number,
required: true,
},
availableRates: {
type: Array as PropType<number[]>,
required: true,
},
},
setup(props) {
if (props.availableRates.length === 0) {
throw new Error("PlaybackRateSwitcher: availableRates must be a non-empty array.");
}
const connection = useConnection();
const roomApi = useRoomApi(connection);
const playbackRate = usePlaybackRate();

const connection = useConnection();
const roomApi = useRoomApi(connection);
function formatRate(rate: number) {
return (
rate.toLocaleString(undefined, {
maximumFractionDigits: 2,
}) + "x"
);
}

function formatRate(rate: number) {
return (
rate.toLocaleString(undefined, {
maximumFractionDigits: 2,
}) + "x"
);
}
function setRate(rate: number) {
roomApi.setPlaybackRate(rate);
}

Check warning on line 39 in client/src/components/controls/PlaybackRateSwitcher.vue

View check run for this annotation

Codecov / codecov/patch

client/src/components/controls/PlaybackRateSwitcher.vue#L37-L39

Added lines #L37 - L39 were not covered by tests

function setRate(rate: number) {
roomApi.setPlaybackRate(rate);
}

const supported = computed(() => {
return props.availableRates.length > 1;
});

return {
formatRate,
supported,
setRate,
};
},
});

export default PlaybackRateSwitcher;
const supported = playbackRate.isPlaybackRateSupported;
</script>

<style lang="scss">
Expand Down
51 changes: 7 additions & 44 deletions client/src/components/controls/VideoControls.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,15 @@
<VolumeControl />
<TimestampDisplay :current-position="truePosition" data-cy="timestamp-display" />
<div class="grow"><!-- Spacer --></div>
<ClosedCaptionsSwitcher
:supported="isCaptionsSupported"
:tracks="store.state.captions.availableTracks"
@enable-cc="value => player.setCaptionsEnabled(value)"
@cc-track="value => player.setCaptionsTrack(value)"
/>
<PlaybackRateSwitcher
:current-rate="store.state.room.playbackSpeed"
:available-rates="player?.getAvailablePlaybackRates() ?? [1]"
/>
<ClosedCaptionsSwitcher />
<PlaybackRateSwitcher />
<LayoutSwitcher />
</v-row>
</v-col>
</template>

<script lang="ts">
import { defineComponent, PropType, Ref, toRefs } from "vue";
import { defineComponent, PropType } from "vue";

import BasicControls from "./BasicControls.vue";
import ClosedCaptionsSwitcher from "./ClosedCaptionsSwitcher.vue";
Expand All @@ -40,9 +32,8 @@
import VolumeControl from "./VolumeControl.vue";
import PlaybackRateSwitcher from "./PlaybackRateSwitcher.vue";

import type OmniPlayer from "../players/OmniPlayer.vue";
import type { MediaPlayer, MediaPlayerWithCaptions } from "../players/OmniPlayer.vue";
import { useStore } from "@/store";
import { useMediaPlayer } from "../composables";

export default defineComponent({
name: "VideoControls",
Expand All @@ -64,51 +55,23 @@
type: Number,
required: true,
},
player: {
type: Object as PropType<typeof OmniPlayer | null>,
required: true,
},
controlsVisible: {
type: Boolean,
default: false,
},
isCaptionsSupported: {
type: Boolean,
default: false,
},
mode: {
type: String as PropType<"in-video" | "outside-video">,
default: "in-video",
},
},
emits: [],
setup(props) {
setup() {
const store = useStore();
const { player } = toRefs(props);

function isPlayerPresent(p: Ref<typeof OmniPlayer>): p is Ref<typeof OmniPlayer> {
return !!p.value;
}

function isCaptionsSupported(
p: Ref<MediaPlayer | MediaPlayerWithCaptions>
): p is Ref<MediaPlayerWithCaptions> {
return (player.value as MediaPlayerWithCaptions)?.isCaptionsSupported() ?? false;
}
function getCaptionsTracks(): string[] {
if (!isPlayerPresent(player)) {
return [];
}
if (!isCaptionsSupported(player)) {
return [];
}
return player.value.getCaptionsTracks() ?? [];
}
const player = useMediaPlayer();

Check warning on line 70 in client/src/components/controls/VideoControls.vue

View check run for this annotation

Codecov / codecov/patch

client/src/components/controls/VideoControls.vue#L70

Added line #L70 was not covered by tests

return {
store,

getCaptionsTracks,
player,

Check warning on line 74 in client/src/components/controls/VideoControls.vue

View check run for this annotation

Codecov / codecov/patch

client/src/components/controls/VideoControls.vue#L74

Added line #L74 was not covered by tests
};
},
});
Expand Down
Loading
Loading