shot.vue 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. <template>
  2. <teleport :to="appEl" v-if="appEl">
  3. <!-- <div class="countdown strengthen" v-if="!custom.showBottomBar && countdown">
  4. <p class="title"><span>{{countdown}}</span>秒后开始录制</p>
  5. <p>按ESC可暂停录制</p>
  6. </div> -->
  7. <ui-editor-toolbar :toolbar="custom.showBottomBar" class="shot-ctrl">
  8. <ui-button type="submit" class="btn" @click="close">取消</ui-button>
  9. <ui-button type="primary" class="btn" @click="complete" :class="{disabled: blobs.length === 0}">合并视频</ui-button>
  10. <div class="other" :style="{bottom: barHeight}">
  11. <ui-icon class="icon" type="video1" ctrl @click="start" tip="继续录制" tipV="top" />
  12. </div>
  13. <div class="video-list" v-if="videoList.length">
  14. <div class="layout" :style="{width: `${videoList.length * 130}px`}">
  15. <div v-for="video in videoList" :key="video.cover" class="cover">
  16. <img :src="video.cover">
  17. <ui-icon
  18. type="preview"
  19. ctrl
  20. class="preview"
  21. @click="palyUrl = video.origin"
  22. />
  23. </div>
  24. </div>
  25. </div>
  26. </ui-editor-toolbar>
  27. <Preview
  28. v-if="palyUrl"
  29. :items="[{ type: MediaType.video, url: palyUrl }]"
  30. @close="palyUrl = null"
  31. />
  32. <ShotImiate v-if="!custom.showBottomBar && !countdown" />
  33. </teleport>
  34. </template>
  35. <script lang="ts">
  36. import { ref, defineComponent, onUnmounted, watch, shallowReactive, PropType, computed, watchEffect } from 'vue'
  37. import { VideoRecorder } from '@simaq/core';
  38. import { sdk } from '@/sdk'
  39. import { getVideoCover, togetherCallback } from '@/utils'
  40. import { MediaType, Preview } from '@/components/static-preview/index.vue'
  41. import { Record, getRecordFragmentBlobs } from '@/store'
  42. import ShotImiate from './shot-imitate.vue'
  43. import {
  44. getResource,
  45. showRightCtrlPanoStack,
  46. showRightPanoStack,
  47. showBottomBarStack,
  48. custom,
  49. bottomBarHeightStack,
  50. showHeadBarStack,
  51. showLeftPanoStack
  52. } from '@/env'
  53. import { appEl } from '@/store';
  54. import { useViewStack } from '@/hook';
  55. import { currentModel } from '@/model';
  56. import { Message } from 'bill/expose-common';
  57. export default defineComponent({
  58. props: {
  59. record: {
  60. type: Object as PropType<Record>,
  61. required: true
  62. }
  63. },
  64. emits: {
  65. 'append': (blobs: Blob[]) => true,
  66. 'updateCover': (cover: string) => true,
  67. 'close': () => true,
  68. 'preview': () => true,
  69. 'deleteRecord': () => true
  70. },
  71. setup(props, { emit }) {
  72. const config: any = {
  73. uploadUrl: '',
  74. resolution: '4k',
  75. debug: false,
  76. }
  77. const videoRecorder = new VideoRecorder(config);
  78. const showLeftPano = ref(false)
  79. const showBottomBar = ref(false)
  80. const MAX_SIZE = 2 * 1024 * 1024 * 1024
  81. const MAX_TIME = 30 * 60 * 1000
  82. type VideoItem = { origin: Blob | string, cover: string }
  83. const countdown = ref(0)
  84. let interval: NodeJS.Timer
  85. let recordIng = ref(false)
  86. const start = () => {
  87. if (size.value > MAX_SIZE || pauseTime.value < 2000) {
  88. return Message.warning('已超出限制大小无法继续录制,可保存后继续录制!')
  89. }
  90. showBottomBar.value = false
  91. countdown.value = 2
  92. const timeiffe = () => {
  93. if (--countdown.value <= 0) {
  94. clearInterval(interval)
  95. videoRecorder.startRecord()
  96. recordIng.value = true
  97. } else {
  98. interval = setTimeout(timeiffe, 300)
  99. }
  100. }
  101. timeiffe()
  102. // interval = setInterval(() => {
  103. // if (--countdown.value === 0) {
  104. // clearInterval(interval)
  105. // videoRecorder.startRecord()
  106. // recordIng = true
  107. // }
  108. // }, 1000)
  109. }
  110. const pause = () => {
  111. if (countdown.value === 0 && recordIng.value) {
  112. videoRecorder.endRecord()
  113. recordIng.value = false
  114. }
  115. countdown.value = 0
  116. showBottomBar.value = true
  117. clearInterval(interval)
  118. }
  119. watch(recordIng, (_n, _o, onCleanup) => {
  120. if (recordIng.value) {
  121. const timeout = setTimeout(() => videoRecorder.endRecord(), pauseTime.value)
  122. onCleanup(() => clearTimeout(timeout))
  123. }
  124. })
  125. const blobs: File[] = shallowReactive([])
  126. const size = computed(() => {
  127. console.log(videoList)
  128. return videoList.reduce((t, f) => typeof f.origin === 'string' ? t : t + f.origin.size, 0)
  129. })
  130. const pauseTime = computed(() => (MAX_TIME / MAX_SIZE) * (MAX_SIZE - size.value))
  131. videoRecorder.off('*')
  132. videoRecorder.on('record', blob => {
  133. if (recordIng.value) {
  134. blobs.push(new File([blob], '录屏.mp4', { type: 'video/mp4; codecs=h264' }))
  135. }
  136. })
  137. videoRecorder.on('cancelRecord', pause)
  138. videoRecorder.on('endRecord', pause)
  139. const palyUrl = ref<string | Blob | null>(null)
  140. const videoList: VideoItem[] = shallowReactive([])
  141. let initial = false
  142. watch([blobs, props], async () => {
  143. const existsVideos = []
  144. if (props.record.url) {
  145. existsVideos.push(getResource(props.record.url))
  146. }
  147. const fragmentBlobs = getRecordFragmentBlobs(props.record)
  148. existsVideos.push(...fragmentBlobs, ...blobs)
  149. for (const blob of existsVideos) {
  150. if (videoList.some(item => item.origin === blob)) {
  151. continue
  152. }
  153. const cover = await getVideoCover(blob, 3, 120, 80)
  154. videoList.push({ origin: blob, cover })
  155. }
  156. for (let i = 0; i < videoList.length; i++) {
  157. if (!existsVideos.some(blob => videoList[i].origin === blob)) {
  158. videoList.splice(i--, 1)
  159. }
  160. }
  161. if (!props.record.cover && videoList.length) {
  162. emit('updateCover', videoList[0].cover)
  163. }
  164. if (!initial) {
  165. initial = true
  166. start()
  167. }
  168. }, { immediate: true })
  169. const upHandler = (ev: KeyboardEvent) => ev.code === `Escape` && videoRecorder.endRecord()
  170. document.body.addEventListener('keyup', upHandler, { capture: true })
  171. const complete = () => {
  172. emit('append', blobs)
  173. close()
  174. }
  175. const close = () => {
  176. pause()
  177. emit('close')
  178. }
  179. const barHeight = computed(() => videoList.length ? '180px' : '60px')
  180. onUnmounted(() => {
  181. document.body.removeEventListener('keyup', upHandler, { capture: true })
  182. })
  183. watch(currentModel, () => {
  184. if (currentModel.value) {
  185. showLeftPano.value = false
  186. }
  187. })
  188. useViewStack(() => {
  189. return togetherCallback([
  190. showHeadBarStack.push(ref(false)),
  191. showRightCtrlPanoStack.push(ref(false)),
  192. showRightPanoStack.push(ref(false)),
  193. showBottomBarStack.push(showBottomBar),
  194. bottomBarHeightStack.push(barHeight),
  195. showLeftPanoStack.push(showLeftPano),
  196. close,
  197. () => {
  198. console.log('pop', showBottomBarStack.current)
  199. }
  200. ])
  201. })
  202. return {
  203. MediaType,
  204. complete,
  205. pause,
  206. close,
  207. start,
  208. barHeight,
  209. el: sdk.layout,
  210. blobs,
  211. countdown,
  212. custom,
  213. videoList,
  214. palyUrl,
  215. appEl
  216. }
  217. },
  218. components: {
  219. Preview,
  220. ShotImiate
  221. }
  222. })
  223. </script>
  224. <style lang="scss" src="./style.scss" scoped>
  225. </style>