urls can be edited while playing; refactoring

state split into loading and playing
playing state is global
radonly defaultmedia, remove useless getter, reorder fns
This commit is contained in:
Tomáš Mládek 2020-01-11 10:46:50 +01:00
parent 59c3c3381c
commit 3ae972d09e
3 changed files with 68 additions and 42 deletions

View file

@ -1,3 +1,4 @@
import {PlayingState} from "@/common";
<!--suppress HtmlFormInputWithoutLabel --> <!--suppress HtmlFormInputWithoutLabel -->
<template> <template>
<div id="app"> <div id="app">
@ -5,8 +6,8 @@
<div class="channels-wrapper"> <div class="channels-wrapper">
<div class="channels"> <div class="channels">
<template v-for="i in N_CHANNELS"> <template v-for="i in N_CHANNELS">
<Channel :name="names[i - 1]" :url="urls[i - 1]" :volume="volumes[i-1]" <Channel :name="names[i - 1]" :url="urls[i - 1]" :volume="volumes[i-1]" :playing="playing"
:key="i" :key="i" @loadingState="(state) => {$set(states, i - 1, state)}"
class="channel" ref="channels"/> class="channel" ref="channels"/>
</template> </template>
</div> </div>
@ -20,7 +21,7 @@
</div> </div>
</div> </div>
<div class="controls"> <div class="controls">
<button @click="start">START</button> <button @click="start" :disabled="!startEnabled">START</button>
</div> </div>
</div> </div>
</template> </template>
@ -29,6 +30,7 @@
<script lang="ts"> <script lang="ts">
import {Component, Vue} from "vue-property-decorator"; import {Component, Vue} from "vue-property-decorator";
import Channel from "@/components/Channel.vue"; import Channel from "@/components/Channel.vue";
import {LoadingState, PlayingState} from "@/common";
@Component({ @Component({
components: { components: {
@ -41,18 +43,22 @@ export default class App extends Vue {
private readonly LFO_DEPTH = 33; private readonly LFO_DEPTH = 33;
private readonly LFO_OFFSET = 66; private readonly LFO_OFFSET = 66;
private playing: PlayingState = PlayingState.STOPPED;
private names = ["alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta", "iota", "kappa", "lambda", "mu"]; private names = ["alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta", "iota", "kappa", "lambda", "mu"];
private urls = Array(this.N_CHANNELS).fill(""); private urls = Array(this.N_CHANNELS).fill("");
private volumes = Array(this.N_CHANNELS).fill(50); private volumes = Array(this.N_CHANNELS).fill(50);
private states = Array(this.N_CHANNELS).fill(LoadingState.UNLOADED);
private animateVolumeStart?: Date; private animateVolumeStart?: Date;
private animateVolumeInterval?: number; private animateVolumeInterval?: number;
private defaultMedia = [ private readonly defaultMedia = [
"https://www.youtube.com/watch?v=jX6kn9_U8qk", "https://www.youtube.com/watch?v=jX6kn9_U8qk",
"https://www.youtube.com/watch?v=E77jmtut1Zc", "https://www.youtube.com/watch?v=E77jmtut1Zc",
"https://youtu.be/OW7TH2U4hps" "https://youtu.be/OW7TH2U4hps"
]; ];
private mounted() { private mounted() {
this.defaultMedia.forEach((url, idx) => { this.defaultMedia.forEach((url, idx) => {
this.$set(this.urls, idx, url); this.$set(this.urls, idx, url);
@ -60,10 +66,8 @@ export default class App extends Vue {
} }
private start() { private start() {
(this.$refs.channels as Channel[]).forEach((channel) => { this.playing = PlayingState.PLAYING;
channel.start();
this.animateVolume(); this.animateVolume();
});
} }
private animateVolume() { private animateVolume() {
@ -82,6 +86,10 @@ export default class App extends Vue {
} }
}, 1000 / 60); }, 1000 / 60);
} }
private get startEnabled() {
return this.states.every((state) => state !== LoadingState.LOADING);
}
} }
</script> </script>

12
src/common.ts Normal file
View file

@ -0,0 +1,12 @@
enum LoadingState {
UNLOADED = "unloaded",
LOADING = "loading",
LOADED = "loaded",
}
enum PlayingState {
STOPPED = "stopped",
PLAYING = "playing"
}
export {LoadingState, PlayingState};

View file

@ -1,13 +1,13 @@
<template> <template>
<div :class="['channel', `channel-${state}`]"> <div :class="['channel', `channel-${loadingState}`]">
<div class="volume-wrapper"> <div class="volume-wrapper">
<!--suppress HtmlFormInputWithoutLabel --> <!--suppress HtmlFormInputWithoutLabel -->
<input class="volume" :disabled="state === 'unloaded'" <input class="volume" :disabled="loadingState === 'unloaded'"
type="range" min="0" max="100" v-model="volume"/> type="range" min="0" max="100" v-model="volume"/>
</div> </div>
<div class="description"> <div class="description">
<div class="name">{{name}}</div> <div class="name">{{name}}</div>
<div class="title">{{state === "loading" ? "Loading..." : title}}</div> <div class="title">{{loadingState === "loading" ? "Loading..." : title}}</div>
</div> </div>
<div class="youtube-player" ref="ytpl"/> <div class="youtube-player" ref="ytpl"/>
</div> </div>
@ -20,26 +20,22 @@ import YouTubePlayerFactory from "youtube-player";
// noinspection TypeScriptCheckImport // noinspection TypeScriptCheckImport
import {YouTubePlayer} from "youtube-player/dist/types"; import {YouTubePlayer} from "youtube-player/dist/types";
import PlayerStates from "youtube-player/dist/constants/PlayerStates"; import PlayerStates from "youtube-player/dist/constants/PlayerStates";
import {LoadingState, PlayingState} from "@/common";
enum SOURCE { enum SOURCE {
YouTube, YouTube,
DIRECT DIRECT
} }
enum ChannelState {
UNLOADED = "unloaded",
LOADING = "loading",
READY = "ready",
PLAYING = "playing"
}
@Component @Component
export default class Channel extends Vue { export default class Channel extends Vue {
@Prop() public name: string = "Channel"; @Prop() public name: string = "Channel";
@Prop() public volume: number = 50; @Prop() public volume: number = 50;
@Prop() public url: string | undefined; @Prop() public url: string | undefined;
@Prop() public playing: PlayingState = PlayingState.STOPPED;
private youtubePlayer?: YouTubePlayer; private youtubePlayer?: YouTubePlayer;
private state = ChannelState.UNLOADED; private loadingState = LoadingState.UNLOADED;
private title = ""; private title = "";
private get source() { private get source() {
@ -50,20 +46,18 @@ export default class Channel extends Vue {
} }
} }
private get ytVideoId() { @Watch("playing")
if (this.url) { private onPlayingChange() {
const match = this.url.match(/v=([\w_\-]+)/);
if (match !== null && match.length == 2) {
return match[1];
}
}
}
public start() {
if (this.youtubePlayer) { if (this.youtubePlayer) {
switch (this.playing) {
case PlayingState.PLAYING:
this.youtubePlayer.setVolume(this.volume); this.youtubePlayer.setVolume(this.volume);
this.youtubePlayer.playVideo(); this.youtubePlayer.playVideo();
this.state = ChannelState.PLAYING; break;
case PlayingState.STOPPED:
this.youtubePlayer.stopVideo();
break;
}
} }
} }
@ -77,7 +71,10 @@ export default class Channel extends Vue {
this.youtubePlayer.on("stateChange", (event) => { this.youtubePlayer.on("stateChange", (event) => {
switch (event.data) { switch (event.data) {
case PlayerStates.BUFFERING: case PlayerStates.BUFFERING:
this.state = ChannelState.READY; this.loadingState = LoadingState.LOADED;
if (this.playing == PlayingState.PLAYING) {
this.youtubePlayer!.playVideo();
}
break; break;
case PlayerStates.ENDED: case PlayerStates.ENDED:
this.youtubePlayer!.playVideo(); this.youtubePlayer!.playVideo();
@ -87,7 +84,7 @@ export default class Channel extends Vue {
} }
const videoId = Channel.extractYoutubeId(this.url); const videoId = Channel.extractYoutubeId(this.url);
if (videoId !== undefined) { if (videoId !== undefined) {
this.state = ChannelState.LOADING; this.loadingState = LoadingState.LOADING;
console.log(`Loading YouTube video "${videoId}"`); console.log(`Loading YouTube video "${videoId}"`);
this.youtubePlayer.loadVideoById(videoId); this.youtubePlayer.loadVideoById(videoId);
this.title = "Loading..."; this.title = "Loading...";
@ -101,15 +98,12 @@ export default class Channel extends Vue {
console.error(`Something went wrong trying to parse ${this.url}`); console.error(`Something went wrong trying to parse ${this.url}`);
} }
} else { } else {
this.loadingState = LoadingState.UNLOADED;
this.title = "N/A"; this.title = "N/A";
if (this.youtubePlayer) {
this.youtubePlayer.stopVideo();
} }
} }
private static extractYoutubeId(url: string): string | undefined {
const videoIdMatch = url.match(/(v=|youtu\.be\/)([^&]+)/);
if (videoIdMatch !== null && videoIdMatch.length == 3) {
return videoIdMatch[2];
}
} }
@Watch("volume") @Watch("volume")
@ -119,10 +113,22 @@ export default class Channel extends Vue {
} }
} }
@Watch("loadingState")
private onLoadingStateChange() {
this.$emit("loadingState", this.loadingState);
}
private mounted() { private mounted() {
this.onUrlChange(); this.onUrlChange();
} }
private static extractYoutubeId(url: string): string | undefined {
const videoIdMatch = url.match(/(v=|youtu\.be\/)([^&]+)/);
if (videoIdMatch !== null && videoIdMatch.length == 3) {
return videoIdMatch[2];
}
}
private static fetchYoutubeTitle(videoId: string): Promise<string> { private static fetchYoutubeTitle(videoId: string): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let url = `https://www.googleapis.com/youtube/v3/videos?part=snippet&id=${videoId}`; let url = `https://www.googleapis.com/youtube/v3/videos?part=snippet&id=${videoId}`;