urls can be edited while playing; refactoring

state split into loading and playing
playing state is global
radonly defaultmedia, remove useless getter, reorder fns
master v0.1.0
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 -->
<template>
<div id="app">
@ -5,8 +6,8 @@
<div class="channels-wrapper">
<div class="channels">
<template v-for="i in N_CHANNELS">
<Channel :name="names[i - 1]" :url="urls[i - 1]" :volume="volumes[i-1]"
:key="i"
<Channel :name="names[i - 1]" :url="urls[i - 1]" :volume="volumes[i-1]" :playing="playing"
:key="i" @loadingState="(state) => {$set(states, i - 1, state)}"
class="channel" ref="channels"/>
</template>
</div>
@ -20,7 +21,7 @@
</div>
</div>
<div class="controls">
<button @click="start">START</button>
<button @click="start" :disabled="!startEnabled">START</button>
</div>
</div>
</template>
@ -29,6 +30,7 @@
<script lang="ts">
import {Component, Vue} from "vue-property-decorator";
import Channel from "@/components/Channel.vue";
import {LoadingState, PlayingState} from "@/common";
@Component({
components: {
@ -41,18 +43,22 @@ export default class App extends Vue {
private readonly LFO_DEPTH = 33;
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 urls = Array(this.N_CHANNELS).fill("");
private volumes = Array(this.N_CHANNELS).fill(50);
private states = Array(this.N_CHANNELS).fill(LoadingState.UNLOADED);
private animateVolumeStart?: Date;
private animateVolumeInterval?: number;
private defaultMedia = [
private readonly defaultMedia = [
"https://www.youtube.com/watch?v=jX6kn9_U8qk",
"https://www.youtube.com/watch?v=E77jmtut1Zc",
"https://youtu.be/OW7TH2U4hps"
];
private mounted() {
this.defaultMedia.forEach((url, idx) => {
this.$set(this.urls, idx, url);
@ -60,10 +66,8 @@ export default class App extends Vue {
}
private start() {
(this.$refs.channels as Channel[]).forEach((channel) => {
channel.start();
this.animateVolume();
});
this.playing = PlayingState.PLAYING;
this.animateVolume();
}
private animateVolume() {
@ -82,6 +86,10 @@ export default class App extends Vue {
}
}, 1000 / 60);
}
private get startEnabled() {
return this.states.every((state) => state !== LoadingState.LOADING);
}
}
</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>
<div :class="['channel', `channel-${state}`]">
<div :class="['channel', `channel-${loadingState}`]">
<div class="volume-wrapper">
<!--suppress HtmlFormInputWithoutLabel -->
<input class="volume" :disabled="state === 'unloaded'"
<input class="volume" :disabled="loadingState === 'unloaded'"
type="range" min="0" max="100" v-model="volume"/>
</div>
<div class="description">
<div class="name">{{name}}</div>
<div class="title">{{state === "loading" ? "Loading..." : title}}</div>
<div class="title">{{loadingState === "loading" ? "Loading..." : title}}</div>
</div>
<div class="youtube-player" ref="ytpl"/>
</div>
@ -20,26 +20,22 @@ import YouTubePlayerFactory from "youtube-player";
// noinspection TypeScriptCheckImport
import {YouTubePlayer} from "youtube-player/dist/types";
import PlayerStates from "youtube-player/dist/constants/PlayerStates";
import {LoadingState, PlayingState} from "@/common";
enum SOURCE {
YouTube,
DIRECT
}
enum ChannelState {
UNLOADED = "unloaded",
LOADING = "loading",
READY = "ready",
PLAYING = "playing"
}
@Component
export default class Channel extends Vue {
@Prop() public name: string = "Channel";
@Prop() public volume: number = 50;
@Prop() public url: string | undefined;
@Prop() public playing: PlayingState = PlayingState.STOPPED;
private youtubePlayer?: YouTubePlayer;
private state = ChannelState.UNLOADED;
private loadingState = LoadingState.UNLOADED;
private title = "";
private get source() {
@ -50,20 +46,18 @@ export default class Channel extends Vue {
}
}
private get ytVideoId() {
if (this.url) {
const match = this.url.match(/v=([\w_\-]+)/);
if (match !== null && match.length == 2) {
return match[1];
}
}
}
public start() {
@Watch("playing")
private onPlayingChange() {
if (this.youtubePlayer) {
this.youtubePlayer.setVolume(this.volume);
this.youtubePlayer.playVideo();
this.state = ChannelState.PLAYING;
switch (this.playing) {
case PlayingState.PLAYING:
this.youtubePlayer.setVolume(this.volume);
this.youtubePlayer.playVideo();
break;
case PlayingState.STOPPED:
this.youtubePlayer.stopVideo();
break;
}
}
}
@ -76,8 +70,11 @@ export default class Channel extends Vue {
});
this.youtubePlayer.on("stateChange", (event) => {
switch (event.data) {
case PlayerStates.BUFFERING:
this.state = ChannelState.READY;
case PlayerStates.BUFFERING:
this.loadingState = LoadingState.LOADED;
if (this.playing == PlayingState.PLAYING) {
this.youtubePlayer!.playVideo();
}
break;
case PlayerStates.ENDED:
this.youtubePlayer!.playVideo();
@ -87,7 +84,7 @@ export default class Channel extends Vue {
}
const videoId = Channel.extractYoutubeId(this.url);
if (videoId !== undefined) {
this.state = ChannelState.LOADING;
this.loadingState = LoadingState.LOADING;
console.log(`Loading YouTube video "${videoId}"`);
this.youtubePlayer.loadVideoById(videoId);
this.title = "Loading...";
@ -101,14 +98,11 @@ export default class Channel extends Vue {
console.error(`Something went wrong trying to parse ${this.url}`);
}
} else {
this.loadingState = LoadingState.UNLOADED;
this.title = "N/A";
}
}
private static extractYoutubeId(url: string): string | undefined {
const videoIdMatch = url.match(/(v=|youtu\.be\/)([^&]+)/);
if (videoIdMatch !== null && videoIdMatch.length == 3) {
return videoIdMatch[2];
if (this.youtubePlayer) {
this.youtubePlayer.stopVideo();
}
}
}
@ -119,10 +113,22 @@ export default class Channel extends Vue {
}
}
@Watch("loadingState")
private onLoadingStateChange() {
this.$emit("loadingState", this.loadingState);
}
private mounted() {
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> {
return new Promise((resolve, reject) => {
let url = `https://www.googleapis.com/youtube/v3/videos?part=snippet&id=${videoId}`;