proof of concept
This commit is contained in:
parent
d87f29be8e
commit
c54698a4aa
5 changed files with 209 additions and 74 deletions
41
package-lock.json
generated
41
package-lock.json
generated
|
@ -1042,6 +1042,12 @@
|
||||||
"integrity": "sha512-TfcyNecCz8Z9/s90gBOBniyzZrTru8u2Vp0VZODq4KEBaQu8bfXvu7o/KUOecMpzjbFPUA7aqgSq628Iue5BQg==",
|
"integrity": "sha512-TfcyNecCz8Z9/s90gBOBniyzZrTru8u2Vp0VZODq4KEBaQu8bfXvu7o/KUOecMpzjbFPUA7aqgSq628Iue5BQg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/youtube-player": {
|
||||||
|
"version": "5.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/youtube-player/-/youtube-player-5.5.1.tgz",
|
||||||
|
"integrity": "sha512-6pcJWr/GoMec8a/mhR6uF4DKTPOYD8pnwZiwtxs+holUTIaEarihEX+dgl8NFCZE66698DTE7ehx5JYhF+IhFw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@vue/babel-helper-vue-jsx-merge-props": {
|
"@vue/babel-helper-vue-jsx-merge-props": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.0.0.tgz",
|
||||||
|
@ -6482,6 +6488,11 @@
|
||||||
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
|
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"load-script": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-BJGTngvuVkPuSUp+PaPSuscMbKQ="
|
||||||
|
},
|
||||||
"loader-runner": {
|
"loader-runner": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz",
|
||||||
|
@ -9080,6 +9091,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"sister": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/sister/-/sister-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-p19rtTs+NksBRKW9qn0UhZ8/TUI9BPw9lmtHny+Y3TinWlOa9jWh9xB0AtPSdmOy49NJJJSSe0Ey4C7h0TrcYA=="
|
||||||
|
},
|
||||||
"slash": {
|
"slash": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
|
||||||
|
@ -11047,6 +11063,31 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"youtube-player": {
|
||||||
|
"version": "5.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/youtube-player/-/youtube-player-5.5.2.tgz",
|
||||||
|
"integrity": "sha512-ZGtsemSpXnDky2AUYWgxjaopgB+shFHgXVpiJFeNB5nWEugpW1KWYDaHKuLqh2b67r24GtP6HoSW5swvf0fFIQ==",
|
||||||
|
"requires": {
|
||||||
|
"debug": "^2.6.6",
|
||||||
|
"load-script": "^1.0.0",
|
||||||
|
"sister": "^3.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"requires": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,12 +10,14 @@
|
||||||
"core-js": "^3.4.4",
|
"core-js": "^3.4.4",
|
||||||
"vue": "^2.6.10",
|
"vue": "^2.6.10",
|
||||||
"vue-class-component": "^7.0.2",
|
"vue-class-component": "^7.0.2",
|
||||||
"vue-property-decorator": "^8.3.0"
|
"vue-property-decorator": "^8.3.0",
|
||||||
|
"youtube-player": "^5.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "^4.1.0",
|
"@vue/cli-plugin-babel": "^4.1.0",
|
||||||
"@vue/cli-plugin-typescript": "^4.1.0",
|
"@vue/cli-plugin-typescript": "^4.1.0",
|
||||||
"@vue/cli-service": "^4.1.0",
|
"@vue/cli-service": "^4.1.0",
|
||||||
|
"@types/youtube-player": "^5.5.1",
|
||||||
"typescript": "~3.5.3",
|
"typescript": "~3.5.3",
|
||||||
"vue-template-compiler": "^2.6.10"
|
"vue-template-compiler": "^2.6.10"
|
||||||
}
|
}
|
||||||
|
|
42
src/App.vue
42
src/App.vue
|
@ -1,29 +1,45 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<img alt="Vue logo" src="./assets/logo.png">
|
<div class="channels">
|
||||||
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
|
<template v-for="i in N_CHANNELS">
|
||||||
|
<Channel :url="defaultMedia[i-1]" :key="i" class="channel" ref="channels"/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<button @click="start">START</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!--suppress JSUnusedLocalSymbols, JSMethodCanBeStatic -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from 'vue-property-decorator';
|
import {Component, Vue} from "vue-property-decorator";
|
||||||
import HelloWorld from './components/HelloWorld.vue';
|
import Channel from "@/components/Channel.vue";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
HelloWorld,
|
Channel,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class App extends Vue {}
|
export default class App extends Vue {
|
||||||
|
private readonly N_CHANNELS = 6;
|
||||||
|
|
||||||
|
private defaultMedia = [
|
||||||
|
"https://www.youtube.com/watch?v=q76bMs-NwRk"
|
||||||
|
];
|
||||||
|
|
||||||
|
private start() {
|
||||||
|
(this.$refs.channels as Channel[]).forEach((channel) => {
|
||||||
|
channel.start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#app {
|
.channels {
|
||||||
font-family: 'Avenir', Helvetica, Arial, sans-serif;
|
display: flex;
|
||||||
-webkit-font-smoothing: antialiased;
|
}
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
text-align: center;
|
.channel {
|
||||||
color: #2c3e50;
|
margin: 0 1rem;
|
||||||
margin-top: 60px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
134
src/components/Channel.vue
Normal file
134
src/components/Channel.vue
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
<template>
|
||||||
|
<div :class="['channel', `channel-${state}`]">
|
||||||
|
<!--suppress HtmlFormInputWithoutLabel -->
|
||||||
|
<input class="volume" :disabled="state === 'unloaded'"
|
||||||
|
type="range" min="0" max="100" v-model="volume"/>
|
||||||
|
<div class="youtube-player" ref="ytpl"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!--suppress JSUnusedLocalSymbols -->
|
||||||
|
<script lang="ts">
|
||||||
|
import {Component, Prop, Vue, Watch} from "vue-property-decorator";
|
||||||
|
import YouTubePlayerFactory from "youtube-player";
|
||||||
|
// noinspection TypeScriptCheckImport
|
||||||
|
import {YouTubePlayer} from "youtube-player/dist/types";
|
||||||
|
import PlayerStates from "youtube-player/dist/constants/PlayerStates";
|
||||||
|
|
||||||
|
enum SOURCE {
|
||||||
|
YouTube,
|
||||||
|
DIRECT
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ChannelState {
|
||||||
|
UNLOADED = "unloaded",
|
||||||
|
LOADING = "loading",
|
||||||
|
READY = "ready",
|
||||||
|
PLAYING = "playing"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class Channel extends Vue {
|
||||||
|
@Prop() private url: string | undefined;
|
||||||
|
private youtubePlayer?: YouTubePlayer;
|
||||||
|
private animateVolumeStart?: Date;
|
||||||
|
private animateVolumeInterval?: number;
|
||||||
|
private state = ChannelState.UNLOADED;
|
||||||
|
private volume: number = 50;
|
||||||
|
|
||||||
|
|
||||||
|
private get source() {
|
||||||
|
if (this.url) {
|
||||||
|
if (this.url.includes("youtube") || true) { // TODO
|
||||||
|
return SOURCE.YouTube;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public start() {
|
||||||
|
if (this.youtubePlayer) {
|
||||||
|
this.youtubePlayer.setVolume(this.volume);
|
||||||
|
this.youtubePlayer.playVideo();
|
||||||
|
this.state = ChannelState.PLAYING;
|
||||||
|
this.animateVolume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Watch("url")
|
||||||
|
private onUrlChange() {
|
||||||
|
if (this.url !== undefined) {
|
||||||
|
if (this.youtubePlayer === undefined) {
|
||||||
|
this.youtubePlayer = YouTubePlayerFactory(this.$refs.ytpl as HTMLDivElement);
|
||||||
|
this.youtubePlayer.on("stateChange", (event) => {
|
||||||
|
if (event.data == PlayerStates.BUFFERING) {
|
||||||
|
this.state = ChannelState.READY;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const videoId = this.url.match(/v=([\w_\-]+)/);
|
||||||
|
if (videoId !== null && videoId.length == 2) {
|
||||||
|
this.state = ChannelState.LOADING;
|
||||||
|
console.log(`Loading YouTube video "${videoId[1]}"`);
|
||||||
|
this.youtubePlayer.loadVideoById(videoId[1]);
|
||||||
|
} else {
|
||||||
|
console.error(`Something went wrong trying to parse ${this.url}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Watch("volume")
|
||||||
|
private onVolumeChange() {
|
||||||
|
if (this.youtubePlayer) {
|
||||||
|
this.youtubePlayer.setVolume(this.volume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mounted() {
|
||||||
|
this.onUrlChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
private animateVolume() {
|
||||||
|
clearInterval(this.animateVolumeInterval);
|
||||||
|
const LFO_PERIOD = 3000;
|
||||||
|
this.animateVolumeStart = new Date();
|
||||||
|
this.animateVolumeInterval = setInterval(() => {
|
||||||
|
if (this.animateVolumeStart) {
|
||||||
|
const delta = new Date().getTime() - this.animateVolumeStart.getTime();
|
||||||
|
this.volume = Math.sin(((delta % LFO_PERIOD) / LFO_PERIOD) * 2 * Math.PI) * 50 + 50;
|
||||||
|
}
|
||||||
|
}, 1000 / 60);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!--suppress CssUnusedSymbol -->
|
||||||
|
<style scoped>
|
||||||
|
.channel {
|
||||||
|
border: 1px solid black;
|
||||||
|
width: 20px;
|
||||||
|
height: 170px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-loading {
|
||||||
|
background: grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-unloaded {
|
||||||
|
background: lightgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume {
|
||||||
|
position: relative;
|
||||||
|
width: 150px;
|
||||||
|
height: 15px;
|
||||||
|
top: calc(150px / 2);
|
||||||
|
left: calc(150px / 2 * -1 + 15px / 2);
|
||||||
|
transform: rotate(270deg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.youtube-player {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,58 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="hello">
|
|
||||||
<h1>{{ msg }}</h1>
|
|
||||||
<p>
|
|
||||||
For a guide and recipes on how to configure / customize this project,<br>
|
|
||||||
check out the
|
|
||||||
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
|
|
||||||
</p>
|
|
||||||
<h3>Installed CLI Plugins</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank" rel="noopener">typescript</a></li>
|
|
||||||
</ul>
|
|
||||||
<h3>Essential Links</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
|
|
||||||
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
|
|
||||||
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
|
|
||||||
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
|
|
||||||
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
|
|
||||||
</ul>
|
|
||||||
<h3>Ecosystem</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
|
|
||||||
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
|
|
||||||
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
|
||||||
|
|
||||||
@Component
|
|
||||||
export default class HelloWorld extends Vue {
|
|
||||||
@Prop() private msg!: string;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
|
||||||
<style scoped>
|
|
||||||
h3 {
|
|
||||||
margin: 40px 0 0;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
list-style-type: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
li {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: #42b983;
|
|
||||||
}
|
|
||||||
</style>
|
|
Loading…
Reference in a new issue