shot.vue 7.4 KB

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