watch channel change on player page, ffplayout#351

This commit is contained in:
jb-alvarado 2023-07-14 13:45:48 +02:00
parent 6d356e0cd7
commit eef6310a8d
9 changed files with 141 additions and 87 deletions

View File

@ -3,7 +3,7 @@
<div class="container-fluid"> <div class="container-fluid">
<div class="row control-row"> <div class="row control-row">
<div class="col-3 player-col d-flex flex-column"> <div class="col-3 player-col d-flex flex-column">
<div class="d-flex flex-grow-1 align-items-center"> <div>
<video v-if="streamExtension === 'flv'" ref="httpStreamFlv" class="w-100" controls /> <video v-if="streamExtension === 'flv'" ref="httpStreamFlv" class="w-100" controls />
<VideoPlayer <VideoPlayer
class="live-player" class="live-player"
@ -68,6 +68,7 @@
<div> <div>
<button <button
title="Start Playout Service" title="Start Playout Service"
data-tooltip=tooltip
class="btn btn-primary control-button control-button-play" class="btn btn-primary control-button control-button-play"
:class="isPlaying" :class="isPlaying"
@click="controlProcess('start')" @click="controlProcess('start')"
@ -80,6 +81,7 @@
<div> <div>
<button <button
title="Stop Playout Service" title="Stop Playout Service"
data-tooltip=tooltip
class="btn btn-primary control-button control-button-stop" class="btn btn-primary control-button control-button-stop"
@click="controlProcess('stop')" @click="controlProcess('stop')"
> >
@ -91,6 +93,7 @@
<div> <div>
<button <button
title="Restart Playout Service" title="Restart Playout Service"
data-tooltip=tooltip
class="btn btn-primary control-button control-button-restart" class="btn btn-primary control-button control-button-restart"
@click="controlProcess('restart')" @click="controlProcess('restart')"
> >
@ -103,6 +106,7 @@
<div> <div>
<button <button
title="Jump to last Clip" title="Jump to last Clip"
data-tooltip=tooltip
class="btn btn-primary control-button control-button-control" class="btn btn-primary control-button control-button-control"
@click="controlPlayout('back')" @click="controlPlayout('back')"
> >
@ -114,6 +118,7 @@
<div> <div>
<button <button
title="Reset Playout State" title="Reset Playout State"
data-tooltip=tooltip
class="btn btn-primary control-button control-button-control" class="btn btn-primary control-button control-button-control"
@click="controlPlayout('reset')" @click="controlPlayout('reset')"
> >
@ -125,6 +130,7 @@
<div> <div>
<button <button
title="Jump to next Clip" title="Jump to next Clip"
data-tooltip=tooltip
class="btn btn-primary control-button control-button-control" class="btn btn-primary control-button control-button-control"
@click="controlPlayout('next')" @click="controlPlayout('next')"
> >
@ -142,6 +148,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { storeToRefs } from 'pinia'
import mpegts from 'mpegts.js' import mpegts from 'mpegts.js'
import { useAuth } from '~/stores/auth' import { useAuth } from '~/stores/auth'
import { useConfig } from '~/stores/config' import { useConfig } from '~/stores/config'
@ -152,6 +159,7 @@ const authStore = useAuth()
const configStore = useConfig() const configStore = useConfig()
const playlistStore = usePlaylist() const playlistStore = usePlaylist()
const { filename, secToHMS, timeToSeconds } = stringFormatter() const { filename, secToHMS, timeToSeconds } = stringFormatter()
const { configID } = storeToRefs(useConfig())
const contentType = { 'content-type': 'application/json;charset=UTF-8' } const contentType = { 'content-type': 'application/json;charset=UTF-8' }
const isPlaying = ref('') const isPlaying = ref('')
@ -190,6 +198,22 @@ onMounted(() => {
status() status()
}) })
watch([configID], () => {
breakStatusCheck.value = false
timeStr.value = '00:00:00'
playlistStore.remainingSec = -1
playlistStore.currentClip = ''
playlistStore.ingestRuns = false
playlistStore.currentClipDuration = 0
playlistStore.currentClipIn = 0
playlistStore.currentClipOut = 0
if (timer.value) {
clearTimeout(timer.value)
}
status()
})
onBeforeUnmount(() => { onBeforeUnmount(() => {
breakStatusCheck.value = true breakStatusCheck.value = true

View File

@ -112,8 +112,8 @@ async function addChannel() {
newChannel.service = `ffplayout@${confName}.service` newChannel.service = `ffplayout@${confName}.service`
channels.push(newChannel) channels.push(newChannel)
configStore.updateGuiConfig(channels) configStore.configGui =channels
configStore.updateConfigID(configStore.configGui.length - 1) configStore.configID = configStore.configGui.length - 1
} }
async function onSubmitGui() { async function onSubmitGui() {
@ -154,8 +154,8 @@ async function deleteChannel() {
}) })
config.splice(configStore.configID, 1) config.splice(configStore.configID, 1)
configStore.updateGuiConfig(config) configStore.configGui = config
configStore.updateConfigID(configStore.configGui.length - 1) configStore.configID = configStore.configGui.length - 1
await configStore.getPlayoutConfig() await configStore.getPlayoutConfig()
if (response.status === 200) { if (response.status === 200) {

View File

@ -83,7 +83,7 @@ function logout() {
} }
function selectChannel(index: number) { function selectChannel(index: number) {
configStore.updateConfigID(index) configStore.configID = index
configStore.getPlayoutConfig() configStore.getPlayoutConfig()
} }
</script> </script>

View File

@ -17,6 +17,7 @@
import { useConfig } from '~/stores/config' import { useConfig } from '~/stores/config'
import { useIndex } from '~/stores/index' import { useIndex } from '~/stores/index'
const { $bootstrap } = useNuxtApp()
const configStore = useConfig() const configStore = useConfig()
const indexStore = useIndex() const indexStore = useIndex()
@ -27,6 +28,13 @@ useHead({
} }
}) })
onMounted(() => {
// @ts-ignore
new $bootstrap.Tooltip(document.body, {
selector: "[data-tooltip=tooltip]",
container: "body"
})
})
await configStore.nuxtClientInit() await configStore.nuxtClientInit()
</script> </script>

View File

@ -273,6 +273,7 @@
type="button" type="button"
class="btn btn-primary" class="btn btn-primary"
title="Create Folder" title="Create Folder"
data-tooltip=tooltip
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#folderModal" data-bs-target="#folderModal"
> >
@ -282,6 +283,7 @@
type="button" type="button"
class="btn btn-primary" class="btn btn-primary"
title="Upload File" title="Upload File"
data-tooltip=tooltip
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#uploadModal" data-bs-target="#uploadModal"
> >
@ -438,15 +440,26 @@ const thisUploadModal = ref()
const xhr = ref(new XMLHttpRequest()) const xhr = ref(new XMLHttpRequest())
onMounted(async () => { onMounted(async () => {
let config_extensions = configStore.configPlayout.storage.extensions
let extra_extensions = configStore.configGui[configStore.configID].extra_extensions
if (typeof config_extensions === 'string') {
config_extensions = config_extensions.split(',')
}
if (typeof extra_extensions === 'string') {
extra_extensions = extra_extensions.split(',')
}
const exts = [ const exts = [
...configStore.configPlayout.storage.extensions.split(','), ...config_extensions,
...configStore.configGui[configStore.configID].extra_extensions.split(','), ...extra_extensions,
].map((ext) => { ].map((ext) => {
return `.${ext}` return `.${ext}`
}) })
extensions.value = exts.join(', ') extensions.value = exts.join(', ')
// @ts-ignore // @ts-ignore
thisUploadModal.value = $bootstrap.Modal.getOrCreateInstance(uploadModal.value) thisUploadModal.value = $bootstrap.Modal.getOrCreateInstance(uploadModal.value)
if (!mediaStore.folderTree.parent) { if (!mediaStore.folderTree.parent) {

View File

@ -11,12 +11,13 @@
</div> </div>
<div class="col-2"> <div class="col-2">
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button class="btn btn-primary" title="Save Preset" @click="savePreset()"> <button class="btn btn-primary" title="Save Preset" data-tooltip=tooltip @click="savePreset()">
<i class="bi-cloud-upload" /> <i class="bi-cloud-upload" />
</button> </button>
<button <button
class="btn btn-primary" class="btn btn-primary"
title="New Preset" title="New Preset"
data-tooltip=tooltip
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#createModal" data-bs-target="#createModal"
> >
@ -25,6 +26,7 @@
<button <button
class="btn btn-primary" class="btn btn-primary"
title="Delete Preset" title="Delete Preset"
data-tooltip=tooltip
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#deleteModal" data-bs-target="#deleteModal"
> >
@ -45,6 +47,7 @@
v-model="form.x" v-model="form.x"
type="text" type="text"
title="X Axis" title="X Axis"
data-tooltip=tooltip
placeholder="X" placeholder="X"
required required
/> />
@ -53,6 +56,7 @@
v-model="form.y" v-model="form.y"
type="text" type="text"
title="Y Axis" title="Y Axis"
data-tooltip=tooltip
placeholder="Y" placeholder="Y"
required required
/> />

View File

@ -200,13 +200,20 @@
</splitpanes> </splitpanes>
<div class="btn-group media-button mb-3"> <div class="btn-group media-button mb-3">
<div class="btn btn-primary" title="Copy Playlist" data-bs-toggle="modal" data-bs-target="#copyModal"> <div
class="btn btn-primary"
title="Copy Playlist"
data-bs-toggle="modal"
data-tooltip="tooltip"
data-bs-target="#copyModal"
>
<i class="bi-files" /> <i class="bi-files" />
</div> </div>
<div <div
v-if="!configStore.configPlayout.playlist.loop" v-if="!configStore.configPlayout.playlist.loop"
class="btn btn-primary" class="btn btn-primary"
title="Loop Clips in Playlist" title="Loop Clips in Playlist"
data-tooltip="tooltip"
@click="loopClips()" @click="loopClips()"
> >
<i class="bi-view-stacked" /> <i class="bi-view-stacked" />
@ -214,6 +221,7 @@
<div <div
class="btn btn-primary" class="btn btn-primary"
title="Add (remote) Source to Playlist" title="Add (remote) Source to Playlist"
data-tooltip="tooltip"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#sourceModal" data-bs-target="#sourceModal"
@click="clearNewSource()" @click="clearNewSource()"
@ -223,6 +231,7 @@
<div <div
class="btn btn-primary" class="btn btn-primary"
title="Import text/m3u file" title="Import text/m3u file"
data-tooltip="tooltip"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#importModal" data-bs-target="#importModal"
> >
@ -231,19 +240,26 @@
<div <div
class="btn btn-primary" class="btn btn-primary"
title="Generate a randomized Playlist" title="Generate a randomized Playlist"
data-tooltip="tooltip"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#generateModal" data-bs-target="#generateModal"
@click="mediaStore.getTree('', true)" @click="mediaStore.getTree('', true)"
> >
<i class="bi-sort-down-alt" /> <i class="bi-sort-down-alt" />
</div> </div>
<div class="btn btn-primary" title="Reset Playlist" @click="getPlaylist()"> <div class="btn btn-primary" title="Reset Playlist" data-tooltip="tooltip" @click="getPlaylist()">
<i class="bi-arrow-counterclockwise" /> <i class="bi-arrow-counterclockwise" />
</div> </div>
<div class="btn btn-primary" title="Save Playlist" @click="savePlaylist(listDate)"> <div class="btn btn-primary" title="Save Playlist" data-tooltip="tooltip" @click="savePlaylist(listDate)">
<i class="bi-download" /> <i class="bi-download" />
</div> </div>
<div class="btn btn-primary" title="Delete Playlist" data-bs-toggle="modal" data-bs-target="#deleteModal"> <div
class="btn btn-primary"
title="Delete Playlist"
data-tooltip="tooltip"
data-bs-toggle="modal"
data-bs-target="#deleteModal"
>
<i class="bi-trash" /> <i class="bi-trash" />
</div> </div>
</div> </div>
@ -483,7 +499,7 @@
'/' '/'
), ),
true true
) ),
] ]
" "
> >
@ -550,11 +566,11 @@ const mediaStore = useMedia()
const playlistStore = usePlaylist() const playlistStore = usePlaylist()
definePageMeta({ definePageMeta({
middleware: ['auth'] middleware: ['auth'],
}) })
useHead({ useHead({
title: 'Player | ffplayout' title: 'Player | ffplayout',
}) })
const { configID } = storeToRefs(useConfig()) const { configID } = storeToRefs(useConfig())
@ -575,12 +591,12 @@ const selectedFolders = ref([] as string[])
const generateFromAll = ref(false) const generateFromAll = ref(false)
const browserSortOptions = ref({ const browserSortOptions = ref({
group: { name: 'playlist', pull: 'clone', put: false }, group: { name: 'playlist', pull: 'clone', put: false },
sort: false sort: false,
}) })
const playlistSortOptions = ref({ const playlistSortOptions = ref({
group: 'playlist', group: 'playlist',
animation: 100, animation: 100,
handle: '.grabbing' handle: '.grabbing',
}) })
const newSource = ref({ const newSource = ref({
begin: 0, begin: 0,
@ -591,7 +607,7 @@ const newSource = ref({
custom_filter: '', custom_filter: '',
source: '', source: '',
audio: '', audio: '',
uid: '' uid: '',
} as PlaylistItem) } as PlaylistItem)
onMounted(() => { onMounted(() => {
@ -674,7 +690,7 @@ function cloneClip(event: any) {
source: sourcePath, source: sourcePath,
in: 0, in: 0,
out: mediaStore.folderTree.files[o].duration, out: mediaStore.folderTree.files[o].duration,
duration: mediaStore.folderTree.files[o].duration duration: mediaStore.folderTree.files[o].duration,
}) })
playlistStore.playlist = processPlaylist( playlistStore.playlist = processPlaylist(
@ -719,9 +735,9 @@ function setPreviewData(path: string) {
sources: [ sources: [
{ {
type: `video/${ext}`, type: `video/${ext}`,
src: previewUrl.value src: previewUrl.value,
} },
] ],
} }
} else { } else {
isVideo.value = false isVideo.value = false
@ -760,7 +776,7 @@ function processSource(evt: any) {
custom_filter: '', custom_filter: '',
source: '', source: '',
audio: '', audio: '',
uid: '' uid: '',
} }
} }
@ -775,7 +791,7 @@ function clearNewSource() {
custom_filter: '', custom_filter: '',
source: '', source: '',
audio: '', audio: '',
uid: genUID() uid: genUID(),
} }
} }
@ -791,7 +807,7 @@ function editPlaylistItem(i: number) {
custom_filter: playlistStore.playlist[i].custom_filter, custom_filter: playlistStore.playlist[i].custom_filter,
source: playlistStore.playlist[i].source, source: playlistStore.playlist[i].source,
audio: playlistStore.playlist[i].audio, audio: playlistStore.playlist[i].audio,
uid: playlistStore.playlist[i].uid uid: playlistStore.playlist[i].uid,
} }
} }
@ -811,7 +827,7 @@ function loopClips() {
const tempList = [] const tempList = []
let length = 0 let length = 0
while (length < configStore.playlistLength) { while (length < configStore.playlistLength && playlistStore.playlist.length > 0) {
for (const item of playlistStore.playlist) { for (const item of playlistStore.playlist) {
if (length < configStore.playlistLength) { if (length < configStore.playlistLength) {
tempList.push($_.cloneDeep(item)) tempList.push($_.cloneDeep(item))
@ -843,7 +859,7 @@ async function onSubmitImport(evt: any) {
{ {
method: 'PUT', method: 'PUT',
headers: authStore.authHeader, headers: authStore.authHeader,
body: formData body: formData,
} }
) )
.then(() => { .then(() => {
@ -878,7 +894,7 @@ async function generatePlaylist() {
await $fetch(`/api/playlist/${configStore.configGui[configStore.configID].id}/generate/${listDate.value}`, { await $fetch(`/api/playlist/${configStore.configGui[configStore.configID].id}/generate/${listDate.value}`, {
method: 'POST', method: 'POST',
headers: { ...contentType, ...authStore.authHeader }, headers: { ...contentType, ...authStore.authHeader },
body: (selectedFolders.value.length > 0 && !generateFromAll.value) ? { paths: selectedFolders.value } : null body: selectedFolders.value.length > 0 && !generateFromAll.value ? { paths: selectedFolders.value } : null,
}) })
.then((response: any) => { .then((response: any) => {
playlistStore.playlist = processPlaylist( playlistStore.playlist = processPlaylist(
@ -909,6 +925,10 @@ async function generatePlaylist() {
} }
async function savePlaylist(saveDate: string) { async function savePlaylist(saveDate: string) {
if (playlistStore.playlist.length === 0) {
return
}
playlistStore.playlist = processPlaylist( playlistStore.playlist = processPlaylist(
configStore.startInSec, configStore.startInSec,
configStore.playlistLength, configStore.playlistLength,
@ -923,8 +943,8 @@ async function savePlaylist(saveDate: string) {
body: JSON.stringify({ body: JSON.stringify({
channel: configStore.configGui[configStore.configID].name, channel: configStore.configGui[configStore.configID].name,
date: saveDate, date: saveDate,
program: saveList program: saveList,
}) }),
}) })
.then((response: any) => { .then((response: any) => {
indexStore.alertVariant = 'alert-success' indexStore.alertVariant = 'alert-success'
@ -959,7 +979,7 @@ async function savePlaylist(saveDate: string) {
async function deletePlaylist(playlistDate: string) { async function deletePlaylist(playlistDate: string) {
await $fetch(`/api/playlist/${configStore.configGui[configStore.configID].id}/${playlistDate}`, { await $fetch(`/api/playlist/${configStore.configGui[configStore.configID].id}/${playlistDate}`, {
method: 'DELETE', method: 'DELETE',
headers: { ...contentType, ...authStore.authHeader } headers: { ...contentType, ...authStore.authHeader },
}).then(() => { }).then(() => {
playlistStore.playlist = [] playlistStore.playlist = []

View File

@ -9,7 +9,7 @@ import { useIndex } from '~/stores/index'
interface GuiConfig { interface GuiConfig {
id: number id: number
config_path: string config_path: string
extra_extensions: string extra_extensions: string | string[]
name: string name: string
preview_url: string preview_url: string
service: string service: string
@ -38,42 +38,6 @@ export const useConfig = defineStore('config', {
getters: {}, getters: {},
actions: { actions: {
updateConfigID(id: number) {
this.configID = id
},
updateConfigCount(count: number) {
this.configCount = count
},
updateGuiConfig(config: GuiConfig[]) {
this.configGui = config
},
updateGuiConfigRaw(config: GuiConfig[]) {
this.configGuiRaw = config
},
updateStartTime(sec: number) {
this.startInSec = sec
},
updatePlaylistLength(sec: number) {
this.playlistLength = sec
},
setCurrentUser(user: string) {
this.currentUser = user
},
updateUserConfig(config: User) {
this.configUser = config
},
updateUtcOffset(offset: number) {
this.utcOffset = offset
},
async nuxtClientInit() { async nuxtClientInit() {
const authStore = useAuth() const authStore = useAuth()
@ -102,10 +66,10 @@ export const useConfig = defineStore('config', {
}) })
.then((response) => response.json()) .then((response) => response.json())
.then((objs) => { .then((objs) => {
this.updateUtcOffset(objs[0].utc_offset) this.utcOffset = objs[0].utc_offset
this.updateGuiConfig(objs) this.configGui = objs
this.updateGuiConfigRaw(_.cloneDeep(objs)) this.configGuiRaw = _.cloneDeep(objs)
this.updateConfigCount(objs.length) this.configCount = objs.length
}) })
.catch((e) => { .catch((e) => {
if (statusCode === 401) { if (statusCode === 401) {
@ -116,7 +80,7 @@ export const useConfig = defineStore('config', {
navigateTo('/') navigateTo('/')
} }
this.updateGuiConfig([ this.configGui = [
{ {
id: 1, id: 1,
config_path: '', config_path: '',
@ -126,7 +90,7 @@ export const useConfig = defineStore('config', {
service: '', service: '',
uts_offset: 0, uts_offset: 0,
}, },
]) ]
indexStore.alertMsg = e indexStore.alertMsg = e
indexStore.alertVariant = 'alert-danger' indexStore.alertVariant = 'alert-danger'
@ -164,9 +128,9 @@ export const useConfig = defineStore('config', {
} }
} }
this.updateGuiConfig(guiConfigs) this.configGui = guiConfigs
this.updateGuiConfigRaw(_.cloneDeep(guiConfigs)) this.configGuiRaw = _.cloneDeep(guiConfigs)
this.updateConfigCount(guiConfigs.length) this.configCount = guiConfigs.length
await this.getPlayoutConfig() await this.getPlayoutConfig()
} }
@ -186,13 +150,11 @@ export const useConfig = defineStore('config', {
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
if (data.playlist.day_start) { if (data.playlist.day_start) {
const start = timeToSeconds(data.playlist.day_start) this.startInSec = timeToSeconds(data.playlist.day_start)
this.updateStartTime(start)
} }
if (data.playlist.length) { if (data.playlist.length) {
const length = timeToSeconds(data.playlist.length) this.playlistLength = timeToSeconds(data.playlist.length)
this.updatePlaylistLength(length)
} }
if (data.storage.extensions) { if (data.storage.extensions) {
@ -213,7 +175,12 @@ export const useConfig = defineStore('config', {
const channel = this.configGui[this.configID].id const channel = this.configGui[this.configID].id
const contentType = { 'content-type': 'application/json;charset=UTF-8' } const contentType = { 'content-type': 'application/json;charset=UTF-8' }
obj.storage.extensions = obj.storage.extensions.replace(' ', '').split(/,|;/) this.startInSec = timeToSeconds(obj.playlist.day_start)
this.playlistLength = timeToSeconds(obj.playlist.length)
if (typeof obj.storage.extensions === 'string') {
obj.storage.extensions = obj.storage.extensions.replace(' ', '').split(/,|;/)
}
const update = await fetch(`/api/playout/config/${channel}`, { const update = await fetch(`/api/playout/config/${channel}`, {
method: 'PUT', method: 'PUT',
@ -233,8 +200,8 @@ export const useConfig = defineStore('config', {
}) })
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
this.setCurrentUser(data.username) this.currentUser = data.username
this.updateUserConfig(data) this.configUser = data
}) })
}, },

View File

@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
import { useAuth } from '~/stores/auth' import { useAuth } from '~/stores/auth'
import { useConfig } from '~/stores/config' import { useConfig } from '~/stores/config'
import { useIndex } from '~/stores/index'
export const useMedia = defineStore('media', { export const useMedia = defineStore('media', {
state: () => ({ state: () => ({
@ -17,6 +18,7 @@ export const useMedia = defineStore('media', {
async getTree(path: string, foldersOnly: boolean = false) { async getTree(path: string, foldersOnly: boolean = false) {
const authStore = useAuth() const authStore = useAuth()
const configStore = useConfig() const configStore = useConfig()
const indexStore = useIndex()
const contentType = { 'content-type': 'application/json;charset=UTF-8' } const contentType = { 'content-type': 'application/json;charset=UTF-8' }
const channel = configStore.configGui[configStore.configID].id const channel = configStore.configGui[configStore.configID].id
const crumbs: Crumb[] = [] const crumbs: Crumb[] = []
@ -27,7 +29,23 @@ export const useMedia = defineStore('media', {
headers: { ...contentType, ...authStore.authHeader }, headers: { ...contentType, ...authStore.authHeader },
body: JSON.stringify({ source: path, folders_only: foldersOnly }), body: JSON.stringify({ source: path, folders_only: foldersOnly }),
}) })
.then((response) => response.json()) .then((response) => {
if (response.status === 200) {
return response.json()
} else {
indexStore.alertVariant = 'alert-danger'
indexStore.alertMsg = `Storage not exist!`
indexStore.showAlert = true
return {
source: '',
parent: '',
folders: [],
files: [],
}
}
})
.then((data) => { .then((data) => {
const pathStr = 'Home/' + data.source const pathStr = 'Home/' + data.source
const pathArr = pathStr.split('/') const pathArr = pathStr.split('/')