v5.0.0 - support new API
17
README.md
@ -15,25 +15,22 @@ After installations you have to setup ssl for your **https** connections.
|
|||||||
|
|
||||||
## Some Impressions:
|
## Some Impressions:
|
||||||
#### Login
|
#### Login
|
||||||
![login](/docs/assets/login.png)
|
![login](/docs/images/login.png)
|
||||||
|
|
||||||
#### Landing Page
|
|
||||||
![landing-page](/docs/assets/landing-page.png)
|
|
||||||
|
|
||||||
#### Control Page
|
#### Control Page
|
||||||
![control](/docs/assets/control.png)
|
![control](/docs/images/control.png)
|
||||||
|
|
||||||
#### Media Page
|
#### Media Page
|
||||||
![media](/docs/assets/media.png)
|
![media](/docs/images/media.png)
|
||||||
|
|
||||||
#### Media Page / Upload
|
#### Media Page / Upload
|
||||||
![media-upload](/docs/assets/media-upload.png)
|
![media-upload](/docs/images/media-upload.png)
|
||||||
|
|
||||||
#### Message Page
|
#### Message Page
|
||||||
![message](/docs/assets/message.png)
|
![message](/docs/images/message.png)
|
||||||
|
|
||||||
#### Logging Page
|
#### Logging Page
|
||||||
![logging](/docs/assets/logging.png)
|
![logging](/docs/images/logging.png)
|
||||||
|
|
||||||
#### Configuration Page / GUI
|
#### Configuration Page / GUI
|
||||||
![config-gui](/docs/assets/config-gui.png)
|
![config-gui](/docs/images/config-gui.png)
|
||||||
|
Before Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 160 KiB |
Before Width: | Height: | Size: 77 KiB |
Before Width: | Height: | Size: 378 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 61 KiB |
BIN
docs/images/config-gui.png
Normal file
After Width: | Height: | Size: 64 KiB |
BIN
docs/images/control.png
Normal file
After Width: | Height: | Size: 326 KiB |
BIN
docs/images/logging.png
Normal file
After Width: | Height: | Size: 122 KiB |
BIN
docs/images/login.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
docs/images/media-upload.png
Normal file
After Width: | Height: | Size: 187 KiB |
BIN
docs/images/media.png
Normal file
After Width: | Height: | Size: 209 KiB |
BIN
docs/images/message.png
Normal file
After Width: | Height: | Size: 60 KiB |
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ffplayout",
|
"name": "ffplayout-frontend",
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"description": "web GUI for ffplayout engine",
|
"description": "Web GUI for ffplayout",
|
||||||
"author": "Jonathan Baecker",
|
"author": "Jonathan Baecker",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
v-for="(crumb, index) in crumbs"
|
v-for="(crumb, index) in crumbs"
|
||||||
:key="crumb.key"
|
:key="crumb.key"
|
||||||
:active="index === crumbs.length - 1"
|
:active="index === crumbs.length - 1"
|
||||||
@click="getPath(extensions, crumb.path)"
|
@click="getPath(crumb.path)"
|
||||||
>
|
>
|
||||||
{{ crumb.text }}
|
{{ crumb.text }}
|
||||||
</b-breadcrumb-item>
|
</b-breadcrumb-item>
|
||||||
@ -43,7 +43,7 @@
|
|||||||
<b-icon-folder-fill class="browser-icons" />
|
<b-icon-folder-fill class="browser-icons" />
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col class="browser-item-text">
|
<b-col class="browser-item-text">
|
||||||
<b-link @click="getPath(extensions, `/${folderTree.source}/${folder}`)">
|
<b-link @click="getPath(`/${folderTree.source}/${folder}`)">
|
||||||
{{ folder }}
|
{{ folder }}
|
||||||
</b-link>
|
</b-link>
|
||||||
</b-col>
|
</b-col>
|
||||||
@ -304,7 +304,7 @@ export default {
|
|||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
configID () {
|
configID () {
|
||||||
this.getPath(this.extensions, '')
|
this.getPath('')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -314,14 +314,14 @@ export default {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.extensions = exts.join(',')
|
this.extensions = exts.join(',')
|
||||||
this.getPath(this.extensions, '')
|
this.getPath('')
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async getPath (extensions, path) {
|
async getPath (path) {
|
||||||
this.lastPath = path
|
this.lastPath = path
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
await this.$store.dispatch('media/getTree', { extensions, path })
|
await this.$store.dispatch('media/getTree', { path })
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -369,7 +369,7 @@ export default {
|
|||||||
)
|
)
|
||||||
|
|
||||||
this.$root.$emit('bv::hide::modal', 'folder-modal')
|
this.$root.$emit('bv::hide::modal', 'folder-modal')
|
||||||
this.getPath(this.extensions, this.lastPath)
|
this.getPath(this.lastPath)
|
||||||
},
|
},
|
||||||
|
|
||||||
onCancelCreateFolder (evt) {
|
onCancelCreateFolder (evt) {
|
||||||
@ -429,7 +429,7 @@ export default {
|
|||||||
this.currentNumber = 0
|
this.currentNumber = 0
|
||||||
this.inputPlaceholder = 'Choose files or drop them here...'
|
this.inputPlaceholder = 'Choose files or drop them here...'
|
||||||
this.inputFiles = []
|
this.inputFiles = []
|
||||||
this.getPath(this.extensions, this.lastPath)
|
this.getPath(this.lastPath)
|
||||||
this.$root.$emit('bv::hide::modal', 'upload-modal')
|
this.$root.$emit('bv::hide::modal', 'upload-modal')
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -442,7 +442,7 @@ export default {
|
|||||||
this.inputPlaceholder = 'Choose files or drop them here...'
|
this.inputPlaceholder = 'Choose files or drop them here...'
|
||||||
|
|
||||||
this.cancelTokenSource.cancel('Upload cancelled')
|
this.cancelTokenSource.cancel('Upload cancelled')
|
||||||
this.getPath(this.extensions, this.lastPath)
|
this.getPath(this.lastPath)
|
||||||
|
|
||||||
this.$root.$emit('bv::hide::modal', 'upload-modal')
|
this.$root.$emit('bv::hide::modal', 'upload-modal')
|
||||||
},
|
},
|
||||||
@ -489,7 +489,7 @@ export default {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
this.getPath(this.extensions, this.lastPath)
|
this.getPath(this.lastPath)
|
||||||
|
|
||||||
this.renamePath = ''
|
this.renamePath = ''
|
||||||
this.renameOldName = ''
|
this.renameOldName = ''
|
||||||
@ -537,7 +537,7 @@ export default {
|
|||||||
|
|
||||||
this.$root.$emit('bv::hide::modal', 'delete-modal')
|
this.$root.$emit('bv::hide::modal', 'delete-modal')
|
||||||
|
|
||||||
this.getPath(this.extensions, this.lastPath)
|
this.getPath(this.lastPath)
|
||||||
},
|
},
|
||||||
|
|
||||||
cancelDelete () {
|
cancelDelete () {
|
||||||
|
@ -266,13 +266,13 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
selected (id) {
|
selected (index) {
|
||||||
this.getPreset(id)
|
this.getPreset(index)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
created () {
|
created () {
|
||||||
this.getPreset('')
|
this.getPreset(null)
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@ -284,34 +284,34 @@ export default {
|
|||||||
return (parseFloat(parseInt(num, 16)) / 255).toFixed(2)
|
return (parseFloat(parseInt(num, 16)) / 255).toFixed(2)
|
||||||
},
|
},
|
||||||
|
|
||||||
async getPreset (id) {
|
async getPreset (index) {
|
||||||
const response = await this.$axios.get(`api/presets/${this.configGui[this.configID].id}`)
|
const response = await this.$axios.get(`api/presets/${this.configGui[this.configID].id}`)
|
||||||
|
|
||||||
if (response.data && !id) {
|
if (response.data && index === null) {
|
||||||
this.presets = []
|
this.presets = []
|
||||||
for (const item of response.data) {
|
for (let index = 0; index < response.data.length; index++) {
|
||||||
this.presets.push({ value: item.id, text: item.name })
|
const elem = response.data[index]
|
||||||
|
this.presets.push({ value: index, text: elem.name })
|
||||||
}
|
}
|
||||||
} else if (response.data) {
|
} else if (response.data) {
|
||||||
id -= 1
|
const fColor = response.data[index].fontcolor.split('@')
|
||||||
const fColor = response.data[id].fontcolor.split('@')
|
const bColor = response.data[index].boxcolor.split('@')
|
||||||
const bColor = response.data[id].boxcolor.split('@')
|
|
||||||
|
|
||||||
this.form = {
|
this.form = {
|
||||||
id: response.data[id].id,
|
id: response.data[index].id,
|
||||||
name: response.data[id].name,
|
name: response.data[index].name,
|
||||||
text: response.data[id].text,
|
text: response.data[index].text,
|
||||||
x: response.data[id].x,
|
x: response.data[index].x,
|
||||||
y: response.data[id].y,
|
y: response.data[index].y,
|
||||||
fontSize: response.data[id].fontsize,
|
fontSize: response.data[index].fontsize,
|
||||||
fontSpacing: response.data[id].line_spacing,
|
fontSpacing: response.data[index].line_spacing,
|
||||||
fontColor: fColor[0],
|
fontColor: fColor[0],
|
||||||
fontAlpha: (fColor[1]) ? this.hexToDec(fColor[1]) : 1.0,
|
fontAlpha: (fColor[1]) ? this.hexToDec(fColor[1]) : 1.0,
|
||||||
showBox: response.data[id].box,
|
showBox: response.data[index].box,
|
||||||
boxColor: bColor[0],
|
boxColor: bColor[0],
|
||||||
boxAlpha: (bColor[1]) ? this.hexToDec(bColor[1]) : 1.0,
|
boxAlpha: (bColor[1]) ? this.hexToDec(bColor[1]) : 1.0,
|
||||||
border: response.data[id].boxborderw,
|
border: response.data[index].boxborderw,
|
||||||
overallAlpha: response.data[id].alpha
|
overallAlpha: response.data[index].alpha
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -344,7 +344,7 @@ export default {
|
|||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
this.success = true
|
this.success = true
|
||||||
this.getPreset('')
|
this.getPreset(null)
|
||||||
} else {
|
} else {
|
||||||
this.failed = true
|
this.failed = true
|
||||||
}
|
}
|
||||||
@ -394,7 +394,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.$bvModal.hide('delete-modal')
|
this.$bvModal.hide('delete-modal')
|
||||||
this.getPreset('')
|
this.getPreset(null)
|
||||||
},
|
},
|
||||||
|
|
||||||
async submitMessage () {
|
async submitMessage () {
|
||||||
|
113
pages/player.vue
@ -152,7 +152,7 @@
|
|||||||
v-for="(crumb, index) in crumbs"
|
v-for="(crumb, index) in crumbs"
|
||||||
:key="crumb.key"
|
:key="crumb.key"
|
||||||
:active="index === crumbs.length - 1"
|
:active="index === crumbs.length - 1"
|
||||||
@click="getPath(extensions, crumb.path)"
|
@click="getPath(crumb.path)"
|
||||||
>
|
>
|
||||||
{{ crumb.text }}
|
{{ crumb.text }}
|
||||||
</b-breadcrumb-item>
|
</b-breadcrumb-item>
|
||||||
@ -166,7 +166,7 @@
|
|||||||
:key="folder.key"
|
:key="folder.key"
|
||||||
class="browser-item"
|
class="browser-item"
|
||||||
>
|
>
|
||||||
<b-link @click="getPath(extensions, `/${folderTree.source}/${folder}`)">
|
<b-link @click="getPath(`/${folderTree.source}/${folder}`)">
|
||||||
<b-icon-folder-fill class="browser-icons" /> {{ folder }}
|
<b-icon-folder-fill class="browser-icons" /> {{ folder }}
|
||||||
</b-link>
|
</b-link>
|
||||||
</b-list-group-item>
|
</b-list-group-item>
|
||||||
@ -310,6 +310,9 @@
|
|||||||
<b-button v-if="!configPlayout.playlist.loop" v-b-tooltip.hover title="Loop Clips in Playlist" variant="primary" @click="loopClips()">
|
<b-button v-if="!configPlayout.playlist.loop" v-b-tooltip.hover title="Loop Clips in Playlist" variant="primary" @click="loopClips()">
|
||||||
<b-icon-view-stacked />
|
<b-icon-view-stacked />
|
||||||
</b-button>
|
</b-button>
|
||||||
|
<b-button v-b-tooltip.hover title="Add (remote) Source to Playlist" variant="primary" @click="showAddSource()">
|
||||||
|
<b-icon-file-earmark-plus />
|
||||||
|
</b-button>
|
||||||
<b-button v-b-tooltip.hover title="Generate a randomized Playlist" variant="primary" @click="generatePlaylist(listDate)">
|
<b-button v-b-tooltip.hover title="Generate a randomized Playlist" variant="primary" @click="generatePlaylist(listDate)">
|
||||||
<b-icon-sort-down-alt />
|
<b-icon-sort-down-alt />
|
||||||
</b-button>
|
</b-button>
|
||||||
@ -351,6 +354,35 @@
|
|||||||
>
|
>
|
||||||
Delete program from {{ listDate }}
|
Delete program from {{ listDate }}
|
||||||
</b-modal>
|
</b-modal>
|
||||||
|
<b-modal
|
||||||
|
id="add-source-modal"
|
||||||
|
ref="add-source-modal"
|
||||||
|
title="Add (remote) Source"
|
||||||
|
@ok="handleSource"
|
||||||
|
>
|
||||||
|
<form ref="form" @submit.stop.prevent="addSource">
|
||||||
|
<b-form-group label="In" label-for="in-input">
|
||||||
|
<b-form-input id="in-input" v-model.number="newSource.in" type="number" inline />
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-group label="Out" label-for="out-input" invalid-feedback="Out is required">
|
||||||
|
<b-form-input id="out-input" v-model.number="newSource.out" type="number" inline required />
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-group label="Duration" label-for="duration-input" invalid-feedback="Out is required">
|
||||||
|
<b-form-input id="duration-input" v-model.number="newSource.duration" type="number" inline required />
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-group label="Source" label-for="source-input" invalid-feedback="Source is required">
|
||||||
|
<b-form-input id="source-input" v-model="newSource.source" required />
|
||||||
|
</b-form-group>
|
||||||
|
<b-form-checkbox
|
||||||
|
id="ad-input"
|
||||||
|
v-model="newSource.category"
|
||||||
|
value="advertisement"
|
||||||
|
:unchecked-value="newSource.category"
|
||||||
|
>
|
||||||
|
Advertisement
|
||||||
|
</b-form-checkbox>
|
||||||
|
</form>
|
||||||
|
</b-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -398,7 +430,6 @@ export default {
|
|||||||
listDate: this.$dayjs().tz(this.timezone).format('YYYY-MM-DD'),
|
listDate: this.$dayjs().tz(this.timezone).format('YYYY-MM-DD'),
|
||||||
targetDate: this.$dayjs().tz(this.timezone).format('YYYY-MM-DD'),
|
targetDate: this.$dayjs().tz(this.timezone).format('YYYY-MM-DD'),
|
||||||
interval: null,
|
interval: null,
|
||||||
extensions: '',
|
|
||||||
videoOptions: {
|
videoOptions: {
|
||||||
liveui: true,
|
liveui: true,
|
||||||
controls: true,
|
controls: true,
|
||||||
@ -424,6 +455,14 @@ export default {
|
|||||||
scrollOP: {
|
scrollOP: {
|
||||||
suppressScrollX: true,
|
suppressScrollX: true,
|
||||||
minScrollbarLength: 30
|
minScrollbarLength: 30
|
||||||
|
},
|
||||||
|
newSource: {
|
||||||
|
begin: 0,
|
||||||
|
in: 0,
|
||||||
|
out: 0,
|
||||||
|
duration: 0,
|
||||||
|
category: '',
|
||||||
|
source: ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -460,7 +499,7 @@ export default {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
this.getPath(this.extensions, '')
|
this.getPath('')
|
||||||
this.getPlaylist()
|
this.getPlaylist()
|
||||||
setTimeout(() => { scrollTo(this) }, 3000)
|
setTimeout(() => { scrollTo(this) }, 3000)
|
||||||
}
|
}
|
||||||
@ -475,8 +514,7 @@ export default {
|
|||||||
]
|
]
|
||||||
|
|
||||||
this.getStatus()
|
this.getStatus()
|
||||||
this.extensions = this.configPlayout.storage.extensions.join(',')
|
await this.getPath('')
|
||||||
await this.getPath(this.extensions, '')
|
|
||||||
|
|
||||||
const timeInSec = this.$timeToSeconds(this.$dayjs().tz(this.timezone).format('HH:mm:ss'))
|
const timeInSec = this.$timeToSeconds(this.$dayjs().tz(this.timezone).format('HH:mm:ss'))
|
||||||
const listStartSec = this.$timeToSeconds(this.configPlayout.playlist.day_start)
|
const listStartSec = this.$timeToSeconds(this.configPlayout.playlist.day_start)
|
||||||
@ -489,20 +527,15 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
mounted () {
|
mounted () {
|
||||||
// if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
// this.interval = setInterval(() => {
|
this.interval = setInterval(() => {
|
||||||
// this.$store.dispatch('playlist/animClock')
|
this.$store.dispatch('playlist/playoutStat')
|
||||||
// this.getStatus()
|
this.getStatus()
|
||||||
// }, 5000)
|
}, 5000)
|
||||||
// } else {
|
|
||||||
// this.$store.dispatch('playlist/animClock')
|
|
||||||
// }
|
|
||||||
|
|
||||||
this.interval = setInterval(() => {
|
|
||||||
this.$store.dispatch('playlist/playoutStat')
|
this.$store.dispatch('playlist/playoutStat')
|
||||||
this.getStatus()
|
} else {
|
||||||
}, 5000)
|
this.$store.dispatch('playlist/playoutStat')
|
||||||
this.$store.dispatch('playlist/playoutStat')
|
}
|
||||||
|
|
||||||
const streamExtension = this.configGui[this.configID].preview_url.split('.').pop()
|
const streamExtension = this.configGui[this.configID].preview_url.split('.').pop()
|
||||||
let player
|
let player
|
||||||
@ -533,9 +566,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async getPath (extensions, path) {
|
async getPath (path) {
|
||||||
this.browserIsLoading = true
|
this.browserIsLoading = true
|
||||||
await this.$store.dispatch('media/getTree', { extensions, path })
|
await this.$store.dispatch('media/getTree', { path })
|
||||||
this.browserIsLoading = false
|
this.browserIsLoading = false
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -591,14 +624,12 @@ export default {
|
|||||||
this.$root.$emit('bv::show::modal', 'preview-modal')
|
this.$root.$emit('bv::show::modal', 'preview-modal')
|
||||||
},
|
},
|
||||||
|
|
||||||
cloneClip ({ file, duration }) {
|
cloneClip ({ name, duration }) {
|
||||||
const storagePath = this.configPlayout.storage.path
|
const storagePath = this.configPlayout.storage.path
|
||||||
const storagePathArr = storagePath.split('/')
|
const sourcePath = `${storagePath}/${this.folderTree.source}/${name}`.replace('//', '/')
|
||||||
const storageRoot = storagePathArr.pop()
|
|
||||||
const sourcePath = `${storagePathArr.join('/')}/${this.folderTree.tree[0].substring(this.folderTree.tree[0].indexOf(storageRoot))}`
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
source: `${sourcePath}/${file}`,
|
source: sourcePath,
|
||||||
in: 0,
|
in: 0,
|
||||||
out: duration,
|
out: duration,
|
||||||
duration
|
duration
|
||||||
@ -709,6 +740,36 @@ export default {
|
|||||||
|
|
||||||
showDeleteModal () {
|
showDeleteModal () {
|
||||||
this.$root.$emit('bv::show::modal', 'delete-modal')
|
this.$root.$emit('bv::show::modal', 'delete-modal')
|
||||||
|
},
|
||||||
|
|
||||||
|
showAddSource () {
|
||||||
|
this.$bvModal.show('add-source-modal')
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSource (bvModalEvt) {
|
||||||
|
// Prevent modal from closing
|
||||||
|
bvModalEvt.preventDefault()
|
||||||
|
// Trigger submit handler
|
||||||
|
this.addSource()
|
||||||
|
},
|
||||||
|
|
||||||
|
addSource () {
|
||||||
|
const list = this.playlist
|
||||||
|
list.push(this.newSource)
|
||||||
|
this.$store.commit('playlist/UPDATE_PLAYLIST', this.$processPlaylist(this.startInSec, list))
|
||||||
|
|
||||||
|
this.newSource = {
|
||||||
|
begin: 0,
|
||||||
|
in: 0,
|
||||||
|
out: 0,
|
||||||
|
duration: 0,
|
||||||
|
category: '',
|
||||||
|
source: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$bvModal.hide('add-source-modal')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ export const mutations = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
async getTree ({ commit, dispatch, state, rootState }, { extensions, path }) {
|
async getTree ({ commit, dispatch, state, rootState }, { path }) {
|
||||||
const crumbs = []
|
const crumbs = []
|
||||||
let root = '/'
|
let root = '/'
|
||||||
const channel = rootState.config.configGui[rootState.config.configID].id
|
const channel = rootState.config.configGui[rootState.config.configID].id
|
||||||
|