@@ -44,10 +44,85 @@ const rtcNode = shallowRef<RTCNode>()
4444let stateJobId: any
4545let watchDogJonId: any
4646
47+ const isRecordSettingModalOpen = ref (false )
48+ const isRecording = ref (false )
49+ const recordingStartTime = ref (0 )
50+ const recordingTimeStr = ref (' ' )
51+ const recordSize = ref (0 )
52+ const recordSetting = ref ({
53+ mimeType: ' ' ,
54+ bitsPerSecond: 0
55+ })
56+ let videoChunks: Blob [] = []
57+ let fileHandler: FileSystemFileHandle
58+ let mediaRecoder: MediaRecorder
59+
4760useSeoMeta ({
4861 title: t (' btn.monitor' )
4962})
5063
64+ /**
65+ * 开始录制
66+ */
67+ async function startRecording() {
68+ if (isRecording .value || ! curStream .value ) {
69+ return
70+ }
71+ isRecording .value = true
72+ recordingStartTime .value = new Date ().getTime ()
73+ recordSize .value = 0
74+ videoChunks = []
75+ if (isModernFileAPIAvailable ()) {
76+ // 支持现代化文件访问,数据直接写入磁盘
77+ fileHandler = await showSaveFilePicker ({
78+ startIn: ' downloads' ,
79+ suggestedName: ' Video_' + formatDateTime (new Date (), ' yyyyMMddHHmmss' ) + ' .webm'
80+ })
81+ const writer = await fileHandler .createWritable ()
82+
83+ mediaRecoder = new MediaRecorder (curStream .value , { mimeType: recordSetting .value .mimeType })
84+ mediaRecoder .ondataavailable = (ev ) => {
85+ recordingTimeStr .value = formatTime (new Date ().getTime () - recordingStartTime .value )
86+ recordSize .value += ev .data .size
87+ return writer .write (ev .data )
88+ }
89+ mediaRecoder .onerror = console .error
90+ mediaRecoder .onstop = () => writer .close ()
91+ mediaRecoder .start (200 )
92+ } else {
93+ // 不支持现代化文件访问,录制数据保存在内存中(注意可能导致OOM
94+
95+ mediaRecoder = new MediaRecorder (curStream .value , { mimeType: recordSetting .value .mimeType })
96+ mediaRecoder .ondataavailable = (ev ) => {
97+ const timeDuration = new Date ().getTime () - recordingStartTime .value
98+ recordingTimeStr .value = formatTime (timeDuration )
99+ recordSize .value += ev .data .size
100+ videoChunks .push (ev .data )
101+ if (timeDuration >= 600e3 ) {
102+ // 超过十分钟,停止录制,避免OOM
103+ mediaRecoder .stop ()
104+ }
105+ }
106+ mediaRecoder .onerror = console .error
107+ mediaRecoder .onstop = () => {
108+ const videoBlob = new Blob (videoChunks , { type: mediaRecoder .mimeType })
109+ doDownloadFromBlob (
110+ videoBlob ,
111+ ' Video_' + formatDateTime (new Date (), ' yyyyMMddHHmmss' ) + ' .webm'
112+ )
113+ }
114+ mediaRecoder .start (200 )
115+ }
116+ }
117+
118+ /**
119+ * 结束录制
120+ */
121+ function stopRecording() {
122+ mediaRecoder ?.stop ()
123+ isRecording .value = false
124+ }
125+
51126/**
52127 * 关闭媒体流
53128 */
@@ -63,6 +138,7 @@ function closeStream() {
63138 * 断开连接并清理资源
64139 */
65140function disconnect() {
141+ stopRecording ()
66142 clearInterval (stateJobId )
67143 clearInterval (watchDogJonId )
68144 logInfo .value .logs .push ({
@@ -256,6 +332,10 @@ onMounted(() => {
256332 if (! cameraId .value ) {
257333 cameraId .value = ' '
258334 }
335+ const recordMimeTypes = getRecordMimeTypes ()
336+ if (recordMimeTypes .length > 0 ) {
337+ recordSetting .value .mimeType = recordMimeTypes [0 ]
338+ }
259339})
260340
261341onUnmounted (() => {
@@ -271,8 +351,9 @@ onUnmounted(() => {
271351 <UInput
272352 :type =" isShowConnectId ? 'text' : 'password'"
273353 v-model =" cameraId"
274- :disabled =" isConnecting"
354+ :disabled =" isConnecting || logInfo.state === 'connected' "
275355 :ui =" { icon: { trailing: { pointer: '' } } }"
356+ size =" lg"
276357 >
277358 <template #trailing >
278359 <UButton
@@ -317,6 +398,40 @@ onUnmounted(() => {
317398 >{{ $t('btn.disconnect') }}</UButton
318399 >
319400 </div >
401+
402+ <UDivider class =" my-8" :label =" $t('label.record')" />
403+
404+ <div class =" flex flex-row items-center" >
405+ <span >
406+ {{ recordingStartTime === 0 ? '00:00:00' : recordingTimeStr }}
407+ </span >
408+
409+ <span class =" ml-2" >{{ humanFileSize(recordSize) }}</span >
410+
411+ <div class =" flex-1" ></div >
412+
413+ <UButton
414+ variant =" ghost"
415+ color =" gray"
416+ square
417+ class =" mr-2"
418+ :disabled =" isRecording"
419+ @click =" () => (isRecordSettingModalOpen = true)"
420+ ><template #leading ><Icon name =" solar:settings-linear" /></template
421+ ></UButton >
422+
423+ <UButton color =" green" @click =" startRecording" v-if =" !isRecording"
424+ ><template #leading ><icon name =" solar:play-linear" /></template
425+ >{{ $t('btn.startRec') }}</UButton
426+ >
427+ <UButton color =" rose" @click =" stopRecording" v-else
428+ ><template #leading ><Icon name =" solar:stop-linear" /></template
429+ >{{ $t('btn.stopRec') }}</UButton
430+ >
431+ </div >
432+ <div v-show =" !isModernFileAPIAvailable()" class =" text-xs" >
433+ <span class =" text-red-500" >*</span >{{ $t('hint.recHint') }}
434+ </div >
320435 </div >
321436
322437 <div class =" md:flex-1 md:p-4 mt-8 md:mt-0" >
@@ -325,5 +440,23 @@ onUnmounted(() => {
325440 </div >
326441
327442 <LogBar :logInfo =" logInfo" />
443+
444+ <UModal v-model =" isRecordSettingModalOpen" >
445+ <UCard :ui =" { ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }" >
446+ <template #header >
447+ <div >{{ $t('label.recordSettings') }}</div >
448+ </template >
449+
450+ <div class =" space-y-4" >
451+ <UFormGroup :label =" $t('label.format')" >
452+ <USelectMenu :options =" getRecordMimeTypes()" v-model =" recordSetting.mimeType" />
453+ </UFormGroup >
454+
455+ <!-- <UFormGroup :label="$t('label.bps')">
456+ <UInput type="number" v-model="recordSetting.bitsPerSecond" />
457+ </UFormGroup> -->
458+ </div >
459+ </UCard >
460+ </UModal >
328461 </div >
329462</template >
0 commit comments