|
@@ -7,29 +7,42 @@
|
|
|
|
|
|
<ui-editor-toolbar :toolbar="custom.showBottomBar" class="shot-ctrl">
|
|
<ui-editor-toolbar :toolbar="custom.showBottomBar" class="shot-ctrl">
|
|
<ui-button type="submit" class="btn" @click="close">取消</ui-button>
|
|
<ui-button type="submit" class="btn" @click="close">取消</ui-button>
|
|
- <ui-button type="primary" class="btn" @click="complete" :class="{disabled: blobs.length === 0}">合并视频</ui-button>
|
|
|
|
- <div class="other" :style="{bottom: barHeight}">
|
|
|
|
- <ui-icon class="icon" type="video1" ctrl @click="start" tip="继续录制" tipV="top" />
|
|
|
|
|
|
+ <ui-button
|
|
|
|
+ type="primary"
|
|
|
|
+ class="btn"
|
|
|
|
+ @click="complete"
|
|
|
|
+ :class="{ disabled: blobs.length === 0 }"
|
|
|
|
+ >合并视频</ui-button
|
|
|
|
+ >
|
|
|
|
+ <div class="other" :style="{ bottom: barHeight }">
|
|
|
|
+ <ui-icon
|
|
|
|
+ class="icon"
|
|
|
|
+ type="video1"
|
|
|
|
+ ctrl
|
|
|
|
+ @click="start"
|
|
|
|
+ tip="继续录制"
|
|
|
|
+ tipV="top"
|
|
|
|
+ />
|
|
</div>
|
|
</div>
|
|
<div class="video-list" v-if="videoList.length">
|
|
<div class="video-list" v-if="videoList.length">
|
|
- <div class="layout" :style="{width: `${videoList.length * 130}px`}">
|
|
|
|
|
|
+ <div class="layout" :style="{ width: `${videoList.length * 130}px` }">
|
|
<div v-for="video in videoList" :key="video.cover" class="cover">
|
|
<div v-for="video in videoList" :key="video.cover" class="cover">
|
|
- <img :src="video.cover">
|
|
|
|
- <ui-icon
|
|
|
|
- type="preview"
|
|
|
|
- ctrl
|
|
|
|
- class="preview"
|
|
|
|
- @click="palyUrl = video.origin"
|
|
|
|
|
|
+ <img :src="video.cover" />
|
|
|
|
+ <ui-icon
|
|
|
|
+ type="preview"
|
|
|
|
+ ctrl
|
|
|
|
+ class="preview"
|
|
|
|
+ @click="palyUrl = video.origin"
|
|
/>
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</ui-editor-toolbar>
|
|
</ui-editor-toolbar>
|
|
|
|
|
|
- <Preview
|
|
|
|
- v-if="palyUrl"
|
|
|
|
|
|
+ <Preview
|
|
|
|
+ v-if="palyUrl"
|
|
:items="[{ type: MediaType.video, url: palyUrl }]"
|
|
:items="[{ type: MediaType.video, url: palyUrl }]"
|
|
- @close="palyUrl = null"
|
|
|
|
|
|
+ @close="palyUrl = null"
|
|
/>
|
|
/>
|
|
|
|
|
|
<ShotImiate v-if="!custom.showBottomBar && !countdown" />
|
|
<ShotImiate v-if="!custom.showBottomBar && !countdown" />
|
|
@@ -37,78 +50,86 @@
|
|
</template>
|
|
</template>
|
|
|
|
|
|
<script lang="ts">
|
|
<script lang="ts">
|
|
-import { ref, defineComponent, onUnmounted, watch, shallowReactive, PropType, computed, watchEffect } from 'vue'
|
|
|
|
-import { VideoRecorder } from '@simaq/core';
|
|
|
|
-import { sdk } from '@/sdk'
|
|
|
|
-import { getVideoCover, togetherCallback } from '@/utils'
|
|
|
|
-import { MediaType, Preview } from '@/components/static-preview/index.vue'
|
|
|
|
-import { Record, getRecordFragmentBlobs } from '@/store'
|
|
|
|
-import ShotImiate from './shot-imitate.vue'
|
|
|
|
-import {
|
|
|
|
- getResource,
|
|
|
|
- showRightCtrlPanoStack,
|
|
|
|
- showRightPanoStack,
|
|
|
|
- showBottomBarStack,
|
|
|
|
- custom,
|
|
|
|
|
|
+import {
|
|
|
|
+ ref,
|
|
|
|
+ defineComponent,
|
|
|
|
+ onUnmounted,
|
|
|
|
+ watch,
|
|
|
|
+ shallowReactive,
|
|
|
|
+ PropType,
|
|
|
|
+ computed,
|
|
|
|
+ watchEffect,
|
|
|
|
+} from "vue";
|
|
|
|
+import { VideoRecorder } from "simaqcore";
|
|
|
|
+import { sdk } from "@/sdk";
|
|
|
|
+import { getVideoCover, togetherCallback } from "@/utils";
|
|
|
|
+import { MediaType, Preview } from "@/components/static-preview/index.vue";
|
|
|
|
+import { Record, getRecordFragmentBlobs } from "@/store";
|
|
|
|
+import ShotImiate from "./shot-imitate.vue";
|
|
|
|
+import {
|
|
|
|
+ getResource,
|
|
|
|
+ showRightCtrlPanoStack,
|
|
|
|
+ showRightPanoStack,
|
|
|
|
+ showBottomBarStack,
|
|
|
|
+ custom,
|
|
bottomBarHeightStack,
|
|
bottomBarHeightStack,
|
|
showHeadBarStack,
|
|
showHeadBarStack,
|
|
- showLeftPanoStack
|
|
|
|
-} from '@/env'
|
|
|
|
-import { appEl } from '@/store';
|
|
|
|
-import { useViewStack } from '@/hook';
|
|
|
|
-import { currentModel } from '@/model';
|
|
|
|
-import { Message } from 'bill/expose-common';
|
|
|
|
-
|
|
|
|
|
|
+ showLeftPanoStack,
|
|
|
|
+} from "@/env";
|
|
|
|
+import { appEl } from "@/store";
|
|
|
|
+import { useViewStack } from "@/hook";
|
|
|
|
+import { currentModel } from "@/model";
|
|
|
|
+import { Message } from "bill/expose-common";
|
|
|
|
|
|
export default defineComponent({
|
|
export default defineComponent({
|
|
props: {
|
|
props: {
|
|
record: {
|
|
record: {
|
|
type: Object as PropType<Record>,
|
|
type: Object as PropType<Record>,
|
|
- required: true
|
|
|
|
- }
|
|
|
|
|
|
+ required: true,
|
|
|
|
+ },
|
|
},
|
|
},
|
|
emits: {
|
|
emits: {
|
|
- 'append': (blobs: Blob[]) => true,
|
|
|
|
- 'updateCover': (cover: string) => true,
|
|
|
|
- 'close': () => true,
|
|
|
|
- 'preview': () => true,
|
|
|
|
- 'deleteRecord': () => true
|
|
|
|
|
|
+ append: (blobs: Blob[]) => true,
|
|
|
|
+ updateCover: (cover: string) => true,
|
|
|
|
+ close: () => true,
|
|
|
|
+ preview: () => true,
|
|
|
|
+ deleteRecord: () => true,
|
|
},
|
|
},
|
|
setup(props, { emit }) {
|
|
setup(props, { emit }) {
|
|
const config: any = {
|
|
const config: any = {
|
|
- uploadUrl: '',
|
|
|
|
- resolution: '4k',
|
|
|
|
|
|
+ uploadUrl: "",
|
|
|
|
+ resolution: "4k",
|
|
debug: false,
|
|
debug: false,
|
|
- }
|
|
|
|
-
|
|
|
|
|
|
+ };
|
|
|
|
+
|
|
const videoRecorder = new VideoRecorder(config);
|
|
const videoRecorder = new VideoRecorder(config);
|
|
- const showLeftPano = ref(false)
|
|
|
|
- const showBottomBar = ref(false)
|
|
|
|
- const MAX_SIZE = 2 * 1024 * 1024 * 1024
|
|
|
|
- const MAX_TIME = 30 * 60 * 1000
|
|
|
|
|
|
+ const showLeftPano = ref(false);
|
|
|
|
+ const showBottomBar = ref(false);
|
|
|
|
+ const MAX_SIZE = 2 * 1024 * 1024 * 1024;
|
|
|
|
+ const MAX_TIME = 30 * 60 * 1000;
|
|
|
|
|
|
- type VideoItem = { origin: Blob | string, cover: string }
|
|
|
|
|
|
+ type VideoItem = { origin: Blob | string; cover: string };
|
|
|
|
|
|
- const countdown = ref(0)
|
|
|
|
- let interval: NodeJS.Timer
|
|
|
|
- let recordIng = ref(false)
|
|
|
|
|
|
+ const countdown = ref(0);
|
|
|
|
+ let interval: NodeJS.Timer;
|
|
|
|
+ let recordIng = ref(false);
|
|
const start = () => {
|
|
const start = () => {
|
|
if (size.value > MAX_SIZE || pauseTime.value < 2000) {
|
|
if (size.value > MAX_SIZE || pauseTime.value < 2000) {
|
|
- return Message.warning('已超出限制大小无法继续录制,可保存后继续录制!')
|
|
|
|
|
|
+ return Message.warning("已超出限制大小无法继续录制,可保存后继续录制!");
|
|
}
|
|
}
|
|
|
|
|
|
- showBottomBar.value = false
|
|
|
|
- countdown.value = 2
|
|
|
|
|
|
+ showBottomBar.value = false;
|
|
|
|
+ countdown.value = 2;
|
|
const timeiffe = () => {
|
|
const timeiffe = () => {
|
|
if (--countdown.value <= 0) {
|
|
if (--countdown.value <= 0) {
|
|
- clearInterval(interval)
|
|
|
|
- videoRecorder.startRecord()
|
|
|
|
- recordIng.value = true
|
|
|
|
|
|
+ clearInterval(interval);
|
|
|
|
+ videoRecorder.startRecord();
|
|
|
|
+ recordIng.value = true;
|
|
} else {
|
|
} else {
|
|
- interval = setTimeout(timeiffe, 300)
|
|
|
|
|
|
+ interval = setTimeout(timeiffe, 300);
|
|
}
|
|
}
|
|
- }
|
|
|
|
- timeiffe()
|
|
|
|
|
|
+ };
|
|
|
|
+ timeiffe();
|
|
// interval = setInterval(() => {
|
|
// interval = setInterval(() => {
|
|
// if (--countdown.value === 0) {
|
|
// if (--countdown.value === 0) {
|
|
// clearInterval(interval)
|
|
// clearInterval(interval)
|
|
@@ -116,98 +137,104 @@ export default defineComponent({
|
|
// recordIng = true
|
|
// recordIng = true
|
|
// }
|
|
// }
|
|
// }, 1000)
|
|
// }, 1000)
|
|
- }
|
|
|
|
|
|
+ };
|
|
|
|
|
|
const pause = () => {
|
|
const pause = () => {
|
|
if (countdown.value === 0 && recordIng.value) {
|
|
if (countdown.value === 0 && recordIng.value) {
|
|
- videoRecorder.endRecord()
|
|
|
|
- recordIng.value = false
|
|
|
|
|
|
+ videoRecorder.endRecord();
|
|
|
|
+ recordIng.value = false;
|
|
}
|
|
}
|
|
- countdown.value = 0
|
|
|
|
- showBottomBar.value = true
|
|
|
|
- clearInterval(interval)
|
|
|
|
- }
|
|
|
|
|
|
+ countdown.value = 0;
|
|
|
|
+ showBottomBar.value = true;
|
|
|
|
+ clearInterval(interval);
|
|
|
|
+ };
|
|
|
|
|
|
watch(recordIng, (_n, _o, onCleanup) => {
|
|
watch(recordIng, (_n, _o, onCleanup) => {
|
|
if (recordIng.value) {
|
|
if (recordIng.value) {
|
|
- const timeout = setTimeout(() => videoRecorder.endRecord(), pauseTime.value)
|
|
|
|
- onCleanup(() => clearTimeout(timeout))
|
|
|
|
|
|
+ const timeout = setTimeout(() => videoRecorder.endRecord(), pauseTime.value);
|
|
|
|
+ onCleanup(() => clearTimeout(timeout));
|
|
}
|
|
}
|
|
- })
|
|
|
|
|
|
+ });
|
|
|
|
|
|
- const blobs: File[] = shallowReactive([])
|
|
|
|
|
|
+ const blobs: File[] = shallowReactive([]);
|
|
const size = computed(() => {
|
|
const size = computed(() => {
|
|
- console.log(videoList)
|
|
|
|
- return videoList.reduce((t, f) => typeof f.origin === 'string' ? t : t + f.origin.size, 0)
|
|
|
|
- })
|
|
|
|
- const pauseTime = computed(() => (MAX_TIME / MAX_SIZE) * (MAX_SIZE - size.value))
|
|
|
|
- videoRecorder.off('*')
|
|
|
|
- videoRecorder.on('record', blob => {
|
|
|
|
|
|
+ console.log(videoList);
|
|
|
|
+ return videoList.reduce(
|
|
|
|
+ (t, f) => (typeof f.origin === "string" ? t : t + f.origin.size),
|
|
|
|
+ 0
|
|
|
|
+ );
|
|
|
|
+ });
|
|
|
|
+ const pauseTime = computed(() => (MAX_TIME / MAX_SIZE) * (MAX_SIZE - size.value));
|
|
|
|
+ videoRecorder.off("*");
|
|
|
|
+ videoRecorder.on("record", (blob) => {
|
|
if (recordIng.value) {
|
|
if (recordIng.value) {
|
|
- blobs.push(new File([blob], '录屏.mp4', { type: 'video/mp4; codecs=h264' }))
|
|
|
|
|
|
+ blobs.push(new File([blob], "录屏.mp4", { type: "video/mp4; codecs=h264" }));
|
|
}
|
|
}
|
|
- })
|
|
|
|
- videoRecorder.on('cancelRecord', pause)
|
|
|
|
- videoRecorder.on('endRecord', pause)
|
|
|
|
-
|
|
|
|
- const palyUrl = ref<string | Blob | null>(null)
|
|
|
|
- const videoList: VideoItem[] = shallowReactive([])
|
|
|
|
- let initial = false
|
|
|
|
- watch([blobs, props], async () => {
|
|
|
|
- const existsVideos = []
|
|
|
|
-
|
|
|
|
- if (props.record.url) {
|
|
|
|
- existsVideos.push(getResource(props.record.url))
|
|
|
|
- }
|
|
|
|
- const fragmentBlobs = getRecordFragmentBlobs(props.record)
|
|
|
|
- existsVideos.push(...fragmentBlobs, ...blobs)
|
|
|
|
- for (const blob of existsVideos) {
|
|
|
|
- if (videoList.some(item => item.origin === blob)) {
|
|
|
|
- continue
|
|
|
|
|
|
+ });
|
|
|
|
+ videoRecorder.on("cancelRecord", pause);
|
|
|
|
+ videoRecorder.on("endRecord", pause);
|
|
|
|
+
|
|
|
|
+ const palyUrl = ref<string | Blob | null>(null);
|
|
|
|
+ const videoList: VideoItem[] = shallowReactive([]);
|
|
|
|
+ let initial = false;
|
|
|
|
+ watch(
|
|
|
|
+ [blobs, props],
|
|
|
|
+ async () => {
|
|
|
|
+ const existsVideos = [];
|
|
|
|
+
|
|
|
|
+ if (props.record.url) {
|
|
|
|
+ existsVideos.push(getResource(props.record.url));
|
|
}
|
|
}
|
|
- const cover = await getVideoCover(blob, 3, 120, 80)
|
|
|
|
- videoList.push({ origin: blob, cover })
|
|
|
|
- }
|
|
|
|
- for (let i = 0; i < videoList.length; i++) {
|
|
|
|
- if (!existsVideos.some(blob => videoList[i].origin === blob)) {
|
|
|
|
- videoList.splice(i--, 1)
|
|
|
|
|
|
+ const fragmentBlobs = getRecordFragmentBlobs(props.record);
|
|
|
|
+ existsVideos.push(...fragmentBlobs, ...blobs);
|
|
|
|
+ for (const blob of existsVideos) {
|
|
|
|
+ if (videoList.some((item) => item.origin === blob)) {
|
|
|
|
+ continue;
|
|
|
|
+ }
|
|
|
|
+ const cover = await getVideoCover(blob, 3, 120, 80);
|
|
|
|
+ videoList.push({ origin: blob, cover });
|
|
}
|
|
}
|
|
- }
|
|
|
|
- if (!props.record.cover && videoList.length) {
|
|
|
|
- emit('updateCover', videoList[0].cover)
|
|
|
|
- }
|
|
|
|
- if (!initial) {
|
|
|
|
- initial = true
|
|
|
|
- start()
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- }, { immediate: true })
|
|
|
|
|
|
+ for (let i = 0; i < videoList.length; i++) {
|
|
|
|
+ if (!existsVideos.some((blob) => videoList[i].origin === blob)) {
|
|
|
|
+ videoList.splice(i--, 1);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if (!props.record.cover && videoList.length) {
|
|
|
|
+ emit("updateCover", videoList[0].cover);
|
|
|
|
+ }
|
|
|
|
+ if (!initial) {
|
|
|
|
+ initial = true;
|
|
|
|
+ start();
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ { immediate: true }
|
|
|
|
+ );
|
|
|
|
|
|
- const upHandler = (ev: KeyboardEvent) => ev.code === `Escape` && videoRecorder.endRecord()
|
|
|
|
- document.body.addEventListener('keyup', upHandler, { capture: true })
|
|
|
|
|
|
+ const upHandler = (ev: KeyboardEvent) =>
|
|
|
|
+ ev.code === `Escape` && videoRecorder.endRecord();
|
|
|
|
+ document.body.addEventListener("keyup", upHandler, { capture: true });
|
|
|
|
|
|
const complete = () => {
|
|
const complete = () => {
|
|
- emit('append', blobs)
|
|
|
|
- close()
|
|
|
|
- }
|
|
|
|
|
|
+ emit("append", blobs);
|
|
|
|
+ close();
|
|
|
|
+ };
|
|
|
|
|
|
const close = () => {
|
|
const close = () => {
|
|
- pause()
|
|
|
|
- emit('close')
|
|
|
|
- }
|
|
|
|
|
|
+ pause();
|
|
|
|
+ emit("close");
|
|
|
|
+ };
|
|
|
|
|
|
- const barHeight = computed(() => videoList.length ? '180px' : '60px')
|
|
|
|
-
|
|
|
|
- onUnmounted(() => {
|
|
|
|
- document.body.removeEventListener('keyup', upHandler, { capture: true })
|
|
|
|
- })
|
|
|
|
|
|
+ const barHeight = computed(() => (videoList.length ? "180px" : "60px"));
|
|
|
|
|
|
|
|
+ onUnmounted(() => {
|
|
|
|
+ document.body.removeEventListener("keyup", upHandler, { capture: true });
|
|
|
|
+ });
|
|
|
|
|
|
watch(currentModel, () => {
|
|
watch(currentModel, () => {
|
|
if (currentModel.value) {
|
|
if (currentModel.value) {
|
|
- showLeftPano.value = false
|
|
|
|
|
|
+ showLeftPano.value = false;
|
|
}
|
|
}
|
|
- })
|
|
|
|
|
|
+ });
|
|
useViewStack(() => {
|
|
useViewStack(() => {
|
|
return togetherCallback([
|
|
return togetherCallback([
|
|
showHeadBarStack.push(ref(false)),
|
|
showHeadBarStack.push(ref(false)),
|
|
@@ -218,10 +245,10 @@ export default defineComponent({
|
|
showLeftPanoStack.push(showLeftPano),
|
|
showLeftPanoStack.push(showLeftPano),
|
|
close,
|
|
close,
|
|
() => {
|
|
() => {
|
|
- console.log('pop', showBottomBarStack.current)
|
|
|
|
- }
|
|
|
|
- ])
|
|
|
|
- })
|
|
|
|
|
|
+ console.log("pop", showBottomBarStack.current);
|
|
|
|
+ },
|
|
|
|
+ ]);
|
|
|
|
+ });
|
|
|
|
|
|
return {
|
|
return {
|
|
MediaType,
|
|
MediaType,
|
|
@@ -236,16 +263,14 @@ export default defineComponent({
|
|
custom,
|
|
custom,
|
|
videoList,
|
|
videoList,
|
|
palyUrl,
|
|
palyUrl,
|
|
- appEl
|
|
|
|
- }
|
|
|
|
|
|
+ appEl,
|
|
|
|
+ };
|
|
},
|
|
},
|
|
components: {
|
|
components: {
|
|
Preview,
|
|
Preview,
|
|
- ShotImiate
|
|
|
|
- }
|
|
|
|
-})
|
|
|
|
-
|
|
|
|
|
|
+ ShotImiate,
|
|
|
|
+ },
|
|
|
|
+});
|
|
</script>
|
|
</script>
|
|
|
|
|
|
-<style lang="scss" src="./style.scss" scoped>
|
|
|
|
-</style>
|
|
|
|
|
|
+<style lang="scss" src="./style.scss" scoped></style>
|