Skip to content

Commit d746055

Browse files
committed
feat: record video too
1 parent 25d06cc commit d746055

File tree

1 file changed

+82
-43
lines changed

1 file changed

+82
-43
lines changed

src/App.vue

Lines changed: 82 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,42 +5,39 @@ import { useClamp } from '@vueuse/math';
55
import { AudioMotionAnalyzer } from 'audiomotion-analyzer'
66
import ControlRotary from './ControlRotary.vue';
77
8-
let ctx, tempCanvas, tempCtx, audio
8+
let canvas, ctx, tempCanvas, tempCtx, audio
99
1010
const screen = ref()
11-
const canvasElement = ref()
1211
const video = ref()
1312
14-
const { width, height } = useWindowSize()
15-
const { toggle } = useFullscreen(screen)
13+
const { toggle, isSupported } = useFullscreen(screen)
1614
1715
const paused = ref(false)
1816
const recording = ref(false)
17+
const videoRecording = ref(false)
1918
const recordedWidth = ref(0)
2019
const showVideo = ref(false)
2120
const initiated = ref(false)
2221
const vertical = useStorage('vertical', false)
23-
const invert = useStorage('vertical', false)
22+
const invert = useStorage('inverted', false)
23+
2424
25-
watch([width, height], ([w, h]) => {
26-
if (!canvasElement.value && !tempCanvas) return
27-
canvasElement.value.width = tempCanvas.width = w
28-
canvasElement.value.height = tempCanvas.height = h
25+
const { width, height } = useWindowSize()
26+
const setSize = (w, h) => {
27+
canvas.width = tempCanvas.width = w
28+
canvas.height = tempCanvas.height = h
2929
clear()
30-
})
30+
}
31+
watch([width, height], ([w, h]) => setSize(w, h))
3132
3233
onMounted(() => {
33-
ctx = canvasElement.value.getContext('2d')
34+
canvas = document.createElement('canvas')
35+
ctx = canvas.getContext('2d')
3436
tempCanvas = document.createElement('canvas')
3537
tempCtx = tempCanvas.getContext('2d')
36-
tempCanvas.width = width.value
37-
tempCanvas.height = height.value
38-
clear()
39-
const videostream = canvasElement.value.captureStream();
38+
setSize(width.value, height.value)
39+
const videostream = canvas.captureStream();
4040
video.value.srcObject = videostream;
41-
video.value.play()
42-
.then(() => video.value?.requestPictureInPicture?.())
43-
.catch(error => console.error(error));
4441
});
4542
4643
const smoothing = useClamp(useStorage('smoothing', 0.5), 0, 0.9)
@@ -75,11 +72,49 @@ function initiate() {
7572
const micStream = audio.audioCtx.createMediaStreamSource(stream)
7673
audio.connectInput(micStream)
7774
initiated.value = true
75+
video.value.play()
7876
}).catch((e) => {
7977
console.log('mic denied', e)
8078
})
8179
}
8280
81+
let recorder
82+
83+
const startVideo = () => {
84+
console.log('hello')
85+
videoRecording.value = Date.now()
86+
87+
recorder = new MediaRecorder(video.value.srcObject)
88+
89+
recorder.ondataavailable = (event) => {
90+
const blob = event.data;
91+
const url = URL.createObjectURL(blob);
92+
93+
// Create a new window to display the video
94+
const newWindow = window.open('', '_blank', `width=${width.value},height=${height.value + 1}`);
95+
newWindow.document.write(`
96+
<html style="overscroll-behavior: none;"><body style="margin:0; background: black; position: relative">
97+
<button onclick="saveVideo()" style="position: absolute; top: 1em; left: 1em; font-size: 3em;">Download video</button>
98+
<video controls autoplay >
99+
<source src="${url}" type="video/mp4">
100+
</video>
101+
</body></html>
102+
`);
103+
104+
// Function to handle the "Save Video" button click
105+
newWindow.saveVideo = () => {
106+
const a = newWindow.document.createElement('a');
107+
a.href = url;
108+
a.download = 'recorded_video.mp4';
109+
a.click();
110+
URL.revokeObjectURL(url);
111+
};
112+
};
113+
recorder.start()
114+
}
115+
116+
const stopVideo = () => { videoRecording.value = false; recorder?.stop() }
117+
83118
let offscreenCanvas, offscreenCtx
84119
85120
const startRecording = () => {
@@ -94,15 +129,23 @@ const startRecording = () => {
94129
95130
const stopRecording = () => {
96131
recording.value = false;
97-
98-
// const link = document.createElement('a');
99-
// link.download = 'canvas_recording.png';
100-
// link.href = offscreenCanvas.toDataURL();
101-
// link.click();
102-
132+
const filename = `spectrogram_${new Date().toISOString().slice(0, 19).replace(/T/, '_')}.png`;
103133
offscreenCanvas.toBlob((blob) => {
104134
const blobUrl = window.URL.createObjectURL(blob);
105-
window.open(blobUrl, '_blank');
135+
// window.open(blobUrl, '_blank');
136+
const newWindow = window.open(undefined, '_blank');
137+
if (newWindow) {
138+
newWindow.document.write(`
139+
<html>
140+
<head>
141+
<title>${filename}</title>
142+
</head>
143+
<body style="margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; background: #222222;">
144+
<img src="${blobUrl}" alt="${filename}" style="cursor: pointer; max-width: 100%; max-height: 100vh; object-fit: contain;" onclick="const a = document.createElement('a'); a.href = '${blobUrl}'; a.download = '${filename}'; document.body.appendChild(a); a.click();">
145+
</body>
146+
</html>
147+
`);
148+
}
106149
}, 'image/png');
107150
};
108151
@@ -124,7 +167,7 @@ const recordFrame = () => {
124167
offscreenCtx.drawImage(recTemp, 0, 0)
125168
126169
offscreenCtx.drawImage(
127-
canvasElement.value, // Source canvas
170+
canvas, // Source canvas
128171
width.value - speed.value, 0, // Source area (rightmost line)
129172
speed.value, height.value, // Size of the copied area
130173
recordedWidth.value, 0, // Destination (append at the right)
@@ -141,7 +184,7 @@ const sigmoid = (value) => 1 / (1 + Math.exp(-steepness.value * (value - midpoin
141184
const onCanvasDraw = (instance) => {
142185
if (paused.value) return;
143186
144-
tempCtx.drawImage(canvasElement.value, 0, 0, width.value, height.value, 0, 0, width.value, height.value);
187+
tempCtx.drawImage(canvas, 0, 0, width.value, height.value, 0, 0, width.value, height.value);
145188
146189
const bars = instance.getBars().map(bar => colorFreq(bar.freq, sigmoid(bar.value[0])));
147190
@@ -180,11 +223,7 @@ onKeyStroke('Enter', (e) => { e.preventDefault(); clear(); })
180223
<template lang="pug">
181224
.flex.flex-col.justify-center.bg-black.relative
182225
.fullscreen-container#screen(ref="screen")
183-
canvas#spectrogram.max-w-full(
184-
ref="canvasElement"
185-
:width="width"
186-
:height="height"
187-
)
226+
video.max-w-full(ref="video" @click="paused = !paused")
188227
button.absolute.m-auto.top-0.w-full.h-full.text-white.text-2xl(
189228
title="Press anywhere to start"
190229
v-if="!initiated"
@@ -199,12 +238,23 @@ onKeyStroke('Enter', (e) => { e.preventDefault(); clear(); })
199238
.i-la-arrow-down(v-else)
200239
button.text-xl.select-none.cursor-pointer(@pointerdown="clear()")
201240
.i-la-trash-alt
241+
button.text-xl.select-none.cursor-pointer(@pointerdown="toggle()")
242+
.i-la-expand
243+
button.text-xl.select-none.cursor-pointer.transition(
244+
v-if="video?.requestPictureInPicture"
245+
@pointerdown="video?.requestPictureInPicture?.()")
246+
.i-la-external-link-square-alt
202247
button.text-xl.select-none.cursor-pointer.flex.items-center.gap-1(
203248
:class="{ 'text-red': recording }"
204249
@pointerdown="recording ? stopRecording() : startRecording()")
205250
.i-la-circle(v-if="!recording")
206251
.i-la-dot-circle(v-else)
207252
.p-0.text-sm.font-mono(v-if="recording && recordedWidth") {{ recordedWidth }}px ({{ ((time - recording) / 1000).toFixed(1) }}s)
253+
button.text-xl.select-none.cursor-pointer.flex.items-center.gap-1(
254+
:class="{ 'text-red': videoRecording }"
255+
@pointerdown="!videoRecording ? startVideo() : stopVideo()")
256+
.i-la-video
257+
.p-0.text-sm.font-mono(v-if="videoRecording") {{ ((time - videoRecording) / 1000).toFixed() }}s
208258
209259
210260
.absolute.top-14.left-2.flex.flex-col.text-white.items-center.overscroll-none.overflow-x-hidden.overflow-y-scroll.bg-dark-900.bg-op-20.backdrop-blur.op-40.hover-op-100.transition(v-show="initiated")
@@ -214,18 +264,7 @@ onKeyStroke('Enter', (e) => { e.preventDefault(); clear(); })
214264
ControlRotary(v-model="steepness" :min="3" :max="30" :step="0.0001" :fixed="2" param="CONTRAST")
215265
ControlRotary(v-model="midpoint" :min="0" :max="1" :step=".0001" param="MIDPOINT" :fixed="2")
216266
ControlRotary(v-model="smoothing" :min="0" :max="1" :step=".0001" param="SMOOTH" :fixed="2")
217-
.flex-1
218267
219-
button.top-4.right-4.text-xl.select-none.cursor-pointer.transition(
220-
:style="{ opacity: showVideo ? 1 : 0.2 }"
221-
@pointerdown="showVideo = !showVideo")
222-
.i-la-external-link-square-alt
223-
button.text-xl.select-none.cursor-pointer(@pointerdown="toggle()")
224-
.i-la-expand
225-
.fixed.overflow-clip.text-white.transition.bottom-4.left-18.rounded-xl.overflow-hidden(v-show="showVideo")
226-
.relative
227-
.absolute.p-2.opacity-70.touch-none.select-none.text-md Right click here to enter Picture-In-Picture mode
228-
video.max-h-50.max-w-full(ref="video")
229268
230269
</template>
231270

0 commit comments

Comments
 (0)