|
@@ -0,0 +1,216 @@
|
|
|
+<template>
|
|
|
+ <div class="recorder-mask" v-show="props.show">
|
|
|
+ <video ref="videoEl" autoplay playsinline class="video-preview"></video>
|
|
|
+
|
|
|
+ <div class="controls">
|
|
|
+ <div class="timer-style">
|
|
|
+ <div>长按开始录制</div>
|
|
|
+ <div>{{ recordTime }}s</div>
|
|
|
+ </div>
|
|
|
+ <div class="record-btn"
|
|
|
+ @mousedown.prevent="startRecorderVideo"
|
|
|
+ @mouseup.prevent="stopRecorderVideo"
|
|
|
+ @touchstart.prevent="startRecorderVideo"
|
|
|
+ @touchend.prevent="stopRecorderVideo">
|
|
|
+ <div class="inner-circle" :class="{ recording }"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <button class="close-btn" @click="closeRecorder">×</button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <script setup>
|
|
|
+
|
|
|
+
|
|
|
+ const emit = defineEmits(['close', 'sendVideo']);
|
|
|
+ const props = defineProps({
|
|
|
+ show: Boolean
|
|
|
+ })
|
|
|
+ watch(
|
|
|
+ () => props.show,
|
|
|
+ (newVal,oldVal) => {
|
|
|
+ if (newVal) {
|
|
|
+ // setTimeout(() => {
|
|
|
+ // openVideo();
|
|
|
+ // }, 200);
|
|
|
+ openVideo();
|
|
|
+ }else{
|
|
|
+
|
|
|
+ }
|
|
|
+ }
|
|
|
+);
|
|
|
+ const videoEl = ref(null);
|
|
|
+ const recorderRef = ref(null);
|
|
|
+ const recordTime = ref(0);
|
|
|
+ const recording = ref(false);
|
|
|
+ let timer = null;
|
|
|
+ let chunks = [];
|
|
|
+
|
|
|
+ const startRecorderVideo = () => {
|
|
|
+ if (recorderRef.value) {
|
|
|
+ chunks = [];
|
|
|
+ recorderRef.value.start();
|
|
|
+ recording.value = true;
|
|
|
+ recordTime.value = 0;
|
|
|
+ timer = setInterval(() => recordTime.value++, 1000);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const stopRecorderVideo = () => {
|
|
|
+ if (recorderRef.value && recording.value) {
|
|
|
+ recorderRef.value.stop();
|
|
|
+ recording.value = false;
|
|
|
+ clearInterval(timer);
|
|
|
+
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const closeRecorder = () => {
|
|
|
+ stopRecorderVideo();
|
|
|
+ if (videoEl.value && videoEl.value.srcObject) {
|
|
|
+ videoEl.value.srcObject.getTracks().forEach(track => track.stop());
|
|
|
+ }
|
|
|
+ emit('closeVideo');
|
|
|
+ };
|
|
|
+
|
|
|
+ const openVideo = async () => {
|
|
|
+ recordTime.value = 0;
|
|
|
+ const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' }, audio: false })
|
|
|
+ videoEl.value.srcObject = stream;
|
|
|
+
|
|
|
+ // 创建 canvas
|
|
|
+ const canvas = document.createElement("canvas");
|
|
|
+ const ctx = canvas.getContext("2d");
|
|
|
+
|
|
|
+ // 设置 canvas 尺寸
|
|
|
+ const [track] = stream.getVideoTracks();
|
|
|
+ const settings = track.getSettings();
|
|
|
+ canvas.width = settings.width || 640;
|
|
|
+ canvas.height = settings.height || 480;
|
|
|
+
|
|
|
+ // 取视频第一帧
|
|
|
+ const frame = [];
|
|
|
+
|
|
|
+ // 镜像绘制(左右翻转)
|
|
|
+ function drawFrame() {
|
|
|
+ ctx.save();
|
|
|
+ ctx.scale(-1, 1); // 横向翻转
|
|
|
+ ctx.drawImage(videoEl.value, -canvas.width, 0, canvas.width, canvas.height);
|
|
|
+ if (recording.value && frame.length === 0){
|
|
|
+ canvas.toBlob(blob=>{
|
|
|
+ console.log(blob)
|
|
|
+ frame.push(blob);
|
|
|
+ },"image/png")
|
|
|
+ }
|
|
|
+ ctx.restore();
|
|
|
+ requestAnimationFrame(drawFrame);
|
|
|
+ }
|
|
|
+ drawFrame();
|
|
|
+
|
|
|
+ // 用 canvas 生成新的 video track
|
|
|
+ const canvasStream = canvas.captureStream(30); // 30fps
|
|
|
+ // const audioTrack = stream.getAudioTracks()[0];
|
|
|
+ // if (audioTrack) {
|
|
|
+ // canvasStream.addTrack(audioTrack);
|
|
|
+ // }
|
|
|
+
|
|
|
+ const options = { mimeType: 'video/webm;codecs=vp8,opus' };
|
|
|
+ const recorder = new MediaRecorder(canvasStream, options);
|
|
|
+ recorderRef.value = recorder;
|
|
|
+
|
|
|
+ recorder.ondataavailable = (e) => {
|
|
|
+ if (e.data && e.data.size > 0) chunks.push(e.data);
|
|
|
+ };
|
|
|
+
|
|
|
+ recorder.onstop = () => {
|
|
|
+ const blob = new Blob(chunks, { type: 'video/webm' });
|
|
|
+ if(recordTime.value < 1){
|
|
|
+ recordTime.value = 0;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ videoEl.value.srcObject.getTracks().forEach(track => track.stop());
|
|
|
+ emit('closeVideo');
|
|
|
+ emit('sendVideo', blob);
|
|
|
+ };
|
|
|
+ }
|
|
|
+ // onBeforeUnmount(() => {
|
|
|
+ // clearInterval(timer);
|
|
|
+ // if (videoEl.value && videoEl.value.srcObject) {
|
|
|
+ // videoEl.value.srcObject.getTracks().forEach(track => track.stop());
|
|
|
+ // }
|
|
|
+ // });
|
|
|
+ </script>
|
|
|
+
|
|
|
+ <style scoped>
|
|
|
+ .recorder-mask {
|
|
|
+ position: fixed;
|
|
|
+ top: 0; left: 0;
|
|
|
+ width: 100%; height: 100%;
|
|
|
+ background-color: rgba(0,0,0,0.8);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: flex-end;
|
|
|
+ align-items: center;
|
|
|
+ padding-bottom: 50px;
|
|
|
+ z-index: 999;
|
|
|
+ }
|
|
|
+
|
|
|
+ .video-preview {
|
|
|
+ position: absolute;
|
|
|
+ top: 0; left: 0;
|
|
|
+ width: 100%; height: 100%;
|
|
|
+ object-fit: cover;
|
|
|
+ z-index: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .controls {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ z-index: 99;
|
|
|
+ }
|
|
|
+
|
|
|
+ .record-btn {
|
|
|
+ width: 70px;
|
|
|
+ height: 70px;
|
|
|
+ border-radius: 50%;
|
|
|
+ border: 3px solid white;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+
|
|
|
+ .inner-circle {
|
|
|
+ width: 50px;
|
|
|
+ height: 50px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: white;
|
|
|
+ transition: all 0.2s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .inner-circle.recording {
|
|
|
+ background: red;
|
|
|
+ transform: scale(0.8);
|
|
|
+ }
|
|
|
+
|
|
|
+ .timer-style {
|
|
|
+ color: white;
|
|
|
+ font-size: 16px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .close-btn {
|
|
|
+ position: absolute;
|
|
|
+ top: 40px; right: 20px;
|
|
|
+ font-size: 28px;
|
|
|
+ color: white;
|
|
|
+ background: transparent;
|
|
|
+ border: none;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+
|