Commit 1f2c97c4 by Jonathan Thomas

Integrated Export API, with video playback, download, export again buttons. Load…

Integrated Export API, with video playback, download, export again buttons. Load the most recent export, and poll any active export records for progress.
parent 606eb7f5
<template>
<!-- Clips header & Export button -->
<div class="row mb-3 gx-2 p-2">
<h3 v-if="thumbnailedClips.length > 0">Clips <button type="button" class="btn btn-danger export-btn">Export {{clips.length}} Clips</button></h3>
<h3 v-if="thumbnailedClips.length > 0">Clips <button @click="showExportModal" type="button" class="btn btn-danger export-btn">Export {{clips.length}} Clips</button></h3>
<!-- Loading spinner -->
<div v-if="show_spinner" class="md-5 p-4 text-center">
......@@ -42,10 +42,46 @@
</div>
</div>
</div>
<!-- Export Video Modal -->
<div class="modal fade" ref="exportModal" tabindex="-1" aria-labelledby="exportModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exportModalLabel">Export Video</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- Progress Bar for Export -->
<div v-show="!current_export.output" class="col-sm-12">
<div class="progress align-middle">
<div class="progress-bar" role="progressbar" v-bind:style="{ width: current_export.progress + '%'}" :aria-valuenow="current_export.progress" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
<!-- Preview Video Export -->
<div v-show="current_export.progress == 100.0 && current_export.output" class="col-sm-12">
<video ref="video" loop preload="" poster="" controls class="video-responsive">
<source type="video/mp4" :src="current_export.output">
</video>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" ref="Close" data-bs-dismiss="modal">Cancel</button>
<a v-if="current_export.output" :href="current_export.output" class="btn btn-info" download target="_blank">Download</a>
<button v-if="!current_export.output" type="button" class="btn btn-primary" @click="startExport">Export</button>
<button v-if="current_export.output" type="button" class="btn btn-primary" @click="startExport">Export Again</button>
</div>
</div>
</div>
</div>
</template>
<script>
import {mapActions, mapState, mapMutations, mapGetters} from "vuex"
import { Modal } from "bootstrap"
export default {
name: "Clips.vue",
......@@ -56,6 +92,20 @@ export default {
}
},
methods: {
async startExport() {
let payload = {
"export_type": "video",
"video_format": "mp4",
"video_codec": "libx264",
"video_bitrate": 8000000,
"audio_codec": "aac",
"audio_bitrate": 1920000,
"start_frame": 1,
"project": this.project.url,
"json": {}
}
await this.createExport(payload)
},
deleteClipBtn(clip_id) {
this.deleteClip(clip_id)
},
......@@ -87,12 +137,16 @@ export default {
this.setScrollToClip(null)
}
},
...mapActions(['loadClips', 'createClip', 'deleteClip', 'editClip', 'moveClip']),
showExportModal() {
let myModal = new Modal(this.$refs.exportModal)
myModal.show()
},
...mapActions(['loadClips', 'createClip', 'deleteClip', 'editClip', 'moveClip', 'createExport']),
...mapMutations((['setPreviewClip', 'setClips', 'setScrollToClip']))
},
computed: {
...mapGetters(['totalClipDuration', 'thumbnailedClips']),
...mapState(['clips', 'preview', 'scrollToClip'])
...mapState(['clips', 'preview', 'scrollToClip', 'current_export'])
},
watch: {
scrollToClip() {
......@@ -105,6 +159,12 @@ export default {
}
})
}
},
current_export() {
if (this.current_export.output) {
// Video is available, load video
this.$refs.video.load()
}
}
},
async mounted() {
......@@ -175,4 +235,8 @@ export default {
background-color: #000000;
border-radius: 4px;
}
video {
background-color: #000000;
width: 100%;
}
</style>
\ No newline at end of file
......@@ -163,7 +163,11 @@ export default {
}
},
videoLoaded() {
if (this.preview.file) {
this.loaded_file_id = this.preview.file.id
} else {
this.loaded_file_id = null
}
this.show_spinner = false
this.media_loaded = true
this.media_error = false
......@@ -173,7 +177,11 @@ export default {
}
},
imageLoaded() {
if (this.preview.file) {
this.loaded_file_id = this.preview.file.id
} else {
this.loaded_file_id = null
}
this.show_spinner = false
this.media_loaded = true
this.media_error = false
......
......@@ -10,7 +10,9 @@ export default createStore({
clips: [],
errors: [],
uploads: [],
exports: [],
project: null,
current_export: { progress: 0.0 },
user: null,
preview: {
file: null,
......@@ -160,6 +162,19 @@ export default createStore({
removeUpload(state, uploadID) {
state.uploads = state.uploads.filter(up => up.id != uploadID )
},
setExports(state, exports) {
state.exports = exports
if (state.exports.length == 0) {
state.current_export = { progress: 0.0 }
}
},
setExport(state, exportObj) {
state.exports = [
...state.exports = state.exports.filter(e => e.id != exportObj.id),
exportObj
]
state.current_export = exportObj
},
},
getters: {
isAuthenticated: state => !!state.user,
......@@ -168,6 +183,9 @@ export default createStore({
},
thumbnailedClips: state => {
return state.clips.filter(clip => clip.thumbnail).sort((a, b) => a.position - b.position)
},
activeExports: state => {
return state.exports.filter(e => e.status == "in-progress" || e.status == "pending").sort((a, b) => b.id - a.id)
}
},
actions: {
......@@ -392,8 +410,37 @@ export default createStore({
} catch(err) {
commit('addError', err.response.data)
}
},
async createExport({commit}, payload) {
try {
const response = await instance.post('/exports/', payload)
commit('setExport', response.data)
} catch(err) {
commit('addError', err.response.data)
}
},
async loadExports({commit}, project_id) {
try {
const proj_response = await instance.get(`projects/${project_id}/`)
const sorted_export_urls = proj_response.data.exports.sort().reverse()
if (sorted_export_urls.length > 0) {
// Only get most recent Export record
const export_response = await instance.get(sorted_export_urls[0])
commit('setExport', export_response.data)
}
} catch(err) {
commit('addError', err.response.data)
}
},
async checkExportProgress({commit}, exportObj) {
try {
const export_response = await instance.get(exportObj.url)
commit('setExport', export_response.data)
} catch(err) {
commit('addError', err.response.data)
}
},
},
modules: {
}
})
......@@ -17,7 +17,7 @@
</template>
<script>
import { mapActions, mapMutations, mapState } from "vuex";
import { mapActions, mapMutations, mapState, mapGetters } from "vuex";
import Files from "../components/Files";
import Clips from "../components/Clips";
import Preview from "../components/Preview";
......@@ -27,24 +27,34 @@ export default {
data() {
return {
hasData: false,
exportPollInterval: null
}
},
methods: {
...mapActions(['getProject']),
...mapMutations(['addError', 'setProject'])
pollForExports() {
// Start polling for active exports
if (this.activeExports.length > 0) {
this.checkExportProgress(this.activeExports[0])
}
},
...mapActions(['getProject', 'loadExports', 'checkExportProgress']),
...mapMutations(['addError', 'setProject', 'setExports'])
},
computed: {
id() {
return this.$route.params.id
},
...mapState(['project'])
...mapState(['project']),
...mapGetters(['activeExports'])
},
components: {
Files, Clips, Preview
},
async mounted() {
try {
let results = await this.getProject(this.$route.params.id)
let project_id = this.$route.params.id
await this.loadExports(project_id)
let results = await this.getProject(project_id)
this.setProject(results.data)
this.hasData = true
} catch(err) {
......@@ -52,9 +62,14 @@ export default {
this.addError(err.response.data)
await this.$router.push(`/`)
}
this.exportPollInterval = setInterval(this.pollForExports, 2500)
},
unmounted() {
this.setProject(null)
this.setExports([])
if (this.exportPollInterval) {
clearInterval(this.exportPollInterval)
}
}
}
</script>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment