Explorar el Código

添加拍摄功能

wkw hace 1 mes
padre
commit
900fcb2894

+ 4 - 4
capacitor.config.ts

@@ -53,10 +53,10 @@ let config: CapacitorConfig = {
     }
     // IM证书
     // buildOptions: {
-    //   keystorePath: '1.jks',
-    //   keystoreAlias: 'f62', //  // 生成时指定的别名
-    //   keystorePassword: 'biu',  // 从环境变量读取
-    //   keystoreAliasPassword: '12345678'
+    //   keystorePath: '1.keystore',
+    //   keystoreAlias: 'key0', //  // 生成时指定的别名
+    //   keystorePassword: '123456',  // 从环境变量读取
+    //   keystoreAliasPassword: '123456'
     // }
   }
 };

+ 3 - 3
src/updater/update.json

@@ -1,7 +1,7 @@
 {
-  "version": "2.0.37",
-  "releaseDate": "2025-09-05 09:58:20",
-  "checksum": "b8eac291453eca7837bce8f6951248829fceadac15c13c8e51abd9167966fb4e",
+  "version": "2.0.49",
+  "releaseDate": "2025-09-09 03:04:51",
+  "checksum": "8dbe940247cb4878fb365bbf64ae1c15990f95e4a85043002a30d47351c6b68a",
   "minBinaryVersion": "2.0.0",
   "mandatory": false,
   "upDataDescription": "✨修正一些錯誤。。。。!!!"

+ 216 - 0
src/views/im/chat/components/VideoRecorder/index.vue

@@ -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>
+  

+ 41 - 10
src/views/im/chat/index.vue

@@ -81,6 +81,8 @@
                     :isSender="isSender(item)"
                   />
                 </div>
+                <!-- 拍摄 -->
+                <video class="video-message" :isSender="isSender(item)" controls v-else-if="item.contentType === MsgType.MSG_TYPE.VIDEO" :src="item?.localUrl || IM_PATH + item.url"></video>
 
                 <!-- 语音消息 -->
                 <div class="content" v-if="item.contentType === Constant.REJECT_AUDIO_ONLINE || item.contentType === Constant.REJECT_VIDEO_ONLINE">[对方拒绝]</div>
@@ -247,10 +249,10 @@
           </van-uploader>
           <div>图片</div>
         </div>
-        <!-- <div class="tool-btn">
+        <div class="tool-btn" @click="chatVideo = true">
           <svg-icon class="tool-icon" name="ps" />
           <div>拍摄</div>
-        </div> -->
+        </div>
         <div class="tool-btn" v-if="wsStore.toUserInfo.type == 'user'">
           <svg-icon
             class="tool-icon"
@@ -277,7 +279,11 @@
         </div>
       </div>
     </div>
-
+    <VideoRecorder
+      :show="chatVideo"
+      @closeVideo="chatVideo = false"
+      @sendVideo="handleSendVideo"
+    />
   </div>
 </template>
 
@@ -295,6 +301,7 @@ import * as Constant from "@/common/constant/Constant";
 import { soundVoice } from "@/utils/notifications.js";
 import AtUserList from "./components/AtUserList/index.vue";
 import GroupNotice from "./components/GroupNotice/index.vue";
+import VideoRecorder from "./components/VideoRecorder/index.vue";
 import { messageRevoke } from '@/api/path/im.api';
 
 const IM_PATH = import.meta.env.VITE_IM_PATH_FIlE;
@@ -338,6 +345,7 @@ const noticeRead = ref(false);
 const currentMsg = ref('');
 const showActionSheet = ref(false)
 const actions = ref([])
+const chatVideo = ref(false);
 
 const formatAvatarUrl = (item) => {
   const url = item?.sender?.avatar || item?.fromAvatar || ''
@@ -820,6 +828,31 @@ const beforeRead = (file) => {
   }
   return true;
 };
+// 发送视频
+const handleSendVideo = async (blob) =>{
+  chatVideo.value = false;
+  const arrayBuffer = await blob.arrayBuffer();
+  const audioData = new Uint8Array(arrayBuffer);
+
+  wsStore.sendMessage({
+    // content: "",
+    content: JSON.stringify({ 
+      content: "",
+      msgId: `ms${Date.now()}`, // 消息id
+      quote: quoteMsg.value?.id, // 引用消息id
+      cc: wsStore.toUserInfo.type == 'user'?"":ccMsg.value.join(","), // @成员
+      isTemp: showBurn.value === true, // 消息阅后即焚
+    }), 
+    contentType: MsgType.MSG_TYPE.VIDEO,
+    messageType: wsStore.toUserInfo.type == 'user'?MsgType.MESSAGE_TYPE_USER:(ccMsg.value.length> 0?MsgType.MESSAGE_CC_GROUP:MsgType.MESSAGE_TYPE_GROUP),
+    fileSuffix: "mp4", // 使用webm后缀更准确
+    file: audioData, // 将Uint8Array转为普通数组
+  });
+  cancelQuote(); // 取消引用
+  ccMsg.value = []; // 清空 @成员
+  scrollToBottom();
+  console.log("视频已发送");
+}
 
 //  创建呼叫:开启语音电话: 调试用,正式逻辑读src/views/im/hook/messagesHook.js
 // ==== 1. 发起语音通话 ====
@@ -1077,6 +1110,11 @@ const goDetail = () =>{
           .img-message {
             margin-top: 8px;
           }
+          .video-message{
+            margin-top: 8px;
+            width: 200px;
+            // transform: scaleX(-1);
+          }
         }
       }
       .withdrawal {
@@ -1196,13 +1234,6 @@ const goDetail = () =>{
   border: 2px solid white;
   border-radius: 8px;
 }
-
-.remote-video {
-  width: 100%;
-  height: 100vh;
-  background: black;
-  object-fit: cover;
-}
 /* 按住说话按钮 */
 .hold-talk-btn {
   flex: 1;