wkw 1 сар өмнө
parent
commit
6fb0c5a85e

+ 253 - 195
src/views/im/chat/components/VideoRecorder/index.vue

@@ -1,216 +1,274 @@
 <template>
-    <div class="recorder-mask" v-show="props.show">
+  <div class="recorder-mask" v-show="props.show">
+    <!-- 录制模式 -->
+    <template v-if="!previewUrl">
       <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="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(
+    </template>
+
+    <!-- 预览模式 -->
+    <template v-else>
+      <div class="preview-box">
+        <video 
+          :src="previewUrl" 
+          controls 
+          autoplay 
+          playsinline 
+          class="video-preview preview">
+        </video>
+        <!-- 悬浮按钮 -->
+        <div class="preview-actions">
+          <button class="cancel-btn" @click="redoRecorder">重新录制</button>
+          <button class="send-btn" @click="confirmSend">发送</button>
+        </div>
+      </div>
+    </template>
+
+    <button class="close-btn" @click="closeRecorder">×</button>
+  </div>
+</template>
+
+<script setup>
+const emit = defineEmits(['closeVideo', 'sendVideo']);
+const props = defineProps({
+  show: Boolean,
+});
+
+watch(
   () => props.show,
-  (newVal,oldVal) => {
+  (newVal) => {
     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;
+
+const videoEl = ref(null);
+const recorderRef = ref(null);
+const recordTime = ref(0);
+const recording = ref(false);
+let timer = null;
+let chunks = [];
+let lastBlob = null;
+const previewUrl = ref(null);
+
+const startRecorderVideo = () => {
+  if (recorderRef.value) {
+    chunks = [];
+    recorderRef.value.start();
+    recording.value = true;
+    recordTime.value = 0;
+    timer = setInterval(() => recordTime.value++, 1000);
   }
-  
-  .inner-circle {
-    width: 50px;
-    height: 50px;
-    border-radius: 50%;
-    background: white;
-    transition: all 0.2s;
+};
+
+const stopRecorderVideo = () => {
+  if (recorderRef.value && recording.value) {
+    recorderRef.value.stop();
+    recording.value = false;
+    clearInterval(timer);
   }
-  
-  .inner-circle.recording {
-    background: red;
-    transform: scale(0.8);
+};
+
+const closeRecorder = () => {
+  stopRecorderVideo();
+  if (videoEl.value && videoEl.value.srcObject) {
+    videoEl.value.srcObject.getTracks().forEach((track) => track.stop());
   }
-  
-  .timer-style {
-    color: white;
-    font-size: 16px;
-    margin-bottom: 16px;
-    text-align: center;
+  previewUrl.value = null;
+  emit('closeVideo');
+};
+
+const redoRecorder = () => {
+  previewUrl.value = null;
+  lastBlob = null;
+  openVideo();
+};
+
+const confirmSend = () => {
+  if (lastBlob) {
+    emit('sendVideo', lastBlob);
+    closeRecorder();
   }
-  
-  .close-btn {
-    position: absolute;
-    top: 40px; right: 20px;
-    font-size: 28px;
-    color: white;
-    background: transparent;
-    border: none;
-    cursor: pointer;
+};
+
+const openVideo = async () => {
+  recordTime.value = 0;
+  const stream = await navigator.mediaDevices.getUserMedia({
+    video: { facingMode: 'environment' },
+    audio: false,
+  });
+  videoEl.value.srcObject = stream;
+
+  const canvas = document.createElement('canvas');
+  const ctx = canvas.getContext('2d');
+  const [track] = stream.getVideoTracks();
+  const settings = track.getSettings();
+  canvas.width = settings.width || 640;
+  canvas.height = settings.height || 480;
+
+  function drawFrame() {
+    ctx.save();
+    ctx.scale(-1, 1);
+    ctx.drawImage(videoEl.value, -canvas.width, 0, canvas.width, canvas.height);
+    ctx.restore();
+    requestAnimationFrame(drawFrame);
   }
-  </style>
-  
+  drawFrame();
+
+  const canvasStream = canvas.captureStream(30);
+  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 = () => {
+    if (recordTime.value < 1) {
+      recordTime.value = 0;
+      return;
+    }
+    const blob = new Blob(chunks, { type: 'video/webm' });
+    lastBlob = blob;
+    previewUrl.value = URL.createObjectURL(blob);
+    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,
+.preview-controls {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  z-index: 99;
+}
+.preview-controls {
+  flex-direction: row;
+  gap: 20px;
+}
+.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;
+}
+.preview-box {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.video-preview.preview {
+  width: 100%;
+  height: 100%;
+  object-fit: contain;
+  background: black;
+}
+
+.preview-actions {
+  position: absolute;
+  bottom: 120px;
+  /* left: 50%;
+  transform: translateX(-50%); */
+  display: flex;
+  justify-content: center;
+  gap: 30px;
+  z-index: 20;
+}
+
+.cancel-btn,
+.send-btn {
+  padding: 8px 22px;
+  border-radius: 20px;
+  font-size: 16px;
+  border: none;
+  cursor: pointer;
+}
+
+.cancel-btn {
+  background: rgb(255, 255, 255);
+  /* color: white; */
+}
+
+.send-btn {
+  background: #4765dd;
+  color: white;
+}
+</style>