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:
parent
59c3c3381c
commit
3ae972d09e
3 changed files with 68 additions and 42 deletions
24
src/App.vue
24
src/App.vue
|
@ -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
12
src/common.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
enum LoadingState {
|
||||||
|
UNLOADED = "unloaded",
|
||||||
|
LOADING = "loading",
|
||||||
|
LOADED = "loaded",
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PlayingState {
|
||||||
|
STOPPED = "stopped",
|
||||||
|
PLAYING = "playing"
|
||||||
|
}
|
||||||
|
|
||||||
|
export {LoadingState, PlayingState};
|
|
@ -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) {
|
||||||
this.youtubePlayer.setVolume(this.volume);
|
switch (this.playing) {
|
||||||
this.youtubePlayer.playVideo();
|
case PlayingState.PLAYING:
|
||||||
this.state = ChannelState.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) => {
|
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,14 +98,11 @@ 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];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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}`;
|
||||||
|
|
Loading…
Reference in a new issue