Skip to content

Commit

Permalink
feat: recording support
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Apr 18, 2021
1 parent 17dc5b5 commit abb019b
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 88 deletions.
1 change: 0 additions & 1 deletion packages/theme-default/windi.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export default mergeWindicssConfig(
},
shortcuts: {
'bg-main': 'bg-white text-[#181818] dark:(bg-[#121212] text-[#ddd])',
'disabled': 'opacity-25 pointer-events-none',
'abs-t': 'absolute bottom-0 left-0 right-0',
'abs-tl': 'absolute top-0 left-0',
'abs-tr': 'absolute top-0 right-0',
Expand Down
1 change: 1 addition & 0 deletions packages/vite-slides/client/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ function onClick(e: MouseEvent) {
</div>
</div>
<SlideControls v-if="!query.has('print')" />
<WebCamera />
</div>
</template>
Expand Down
113 changes: 26 additions & 87 deletions packages/vite-slides/client/builtin/SlideControls.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<script setup lang="ts">
import { useFullscreen } from '@vueuse/core'
import { computed, ref } from 'vue'
import Recorder from 'recordrtc'
import type { Options as RecorderOptions } from 'recordrtc'
import { isDark, toggleDark, useNavigateControls } from '../logic'
import { recorder } from '../logic/recording'
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen(document.body)
const { hasNext, hasPrev, prev, next, current } = useNavigateControls()
Expand All @@ -16,96 +16,31 @@ const editorLink = computed(() => {
: undefined
})
const recording = ref(false)
const { log } = console
function download(name: string, url: string) {
const a = document.createElement('a')
a.setAttribute('href', url)
a.setAttribute('download', name)
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
}
let recorderCamera: Recorder | undefined
let recorderSlides: Recorder | undefined
const config: RecorderOptions = {
type: 'video',
bitsPerSecond: 4 * 256 * 8 * 1024,
}
async function startRecording() {
recorderCamera = new Recorder(
await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
}),
config,
)
recorderSlides = new Recorder(
// @ts-expect-error
await navigator.mediaDevices.getDisplayMedia({
video: {
aspectRatio: 1.6,
frameRate: 15,
width: 3840,
height: 2160,
cursor: 'motion',
resizeMode: 'crop-and-scale',
},
}),
config,
)
recorderCamera.startRecording()
recorderSlides.startRecording()
log('started')
}
async function stopRecording() {
if (recorderCamera) {
recorderCamera.stopRecording(() => {
const blob = recorderCamera!.getBlob()
const url = URL.createObjectURL(blob)
download('camera.webm', url)
window.URL.revokeObjectURL(url)
recorderCamera = undefined
})
}
if (recorderSlides) {
recorderSlides.stopRecording(() => {
const blob = recorderSlides!.getBlob()
const url = URL.createObjectURL(blob)
download('screen.webm', url)
window.URL.revokeObjectURL(url)
recorderSlides = undefined
})
}
log('stopped')
}
function toggleRecord() {
recording.value = !recording.value
if (recording.value)
startRecording()
else
stopRecording()
}
const {
recording,
showAvatar,
toggleRecording,
} = recorder
</script>

<template>
<SlidesOverview v-model="showOverview" />
<nav class="opacity-0 pb-4 pt-5 pl-6 pr-4 transition right-0 bottom-0 rounded-tl text-xl flex gap-4 text-gray-400 bg-transparent duration-300 fixed hover:(shadow bg-main opacity-100)">
<nav class="opacity-0 py-2 pl-4 pr-2 transition right-0 bottom-0 rounded-tl text-xl flex gap-1 text-gray-400 bg-transparent duration-300 fixed hover:(shadow bg-main opacity-100)">
<a v-if="editorLink" class="icon-btn" :href="editorLink">
<simple-icons:visualstudiocode />
<carbon:edit />
</a>

<button class="icon-btn" :class="{'text-red-400': recording}" @click="toggleRecord">
<carbon:recording-filled-alt />
<button class="icon-btn" :class="{'text-red-500': recording}" @click="toggleRecording">
<carbon:stop-outline v-if="recording" />
<carbon:video v-else />
</button>

<button
class="icon-btn"
:class="{'text-blue-500': showAvatar && recording, disabled: !recording}"
@click="showAvatar = !showAvatar"
>
<carbon:user-avatar />
</button>

<button class="icon-btn" @click="showOverview = !showOverview">
Expand Down Expand Up @@ -135,7 +70,11 @@ function toggleRecord() {
<style scoped lang="postcss">
.icon-btn {
@apply inline-block cursor-pointer select-none !outline-none;
@apply opacity-75 transition duration-200 ease-in-out align-middle;
@apply hover:(opacity-100 text-teal-600);
@apply opacity-75 transition duration-200 ease-in-out align-middle rounded p-2;
@apply hover:(opacity-100 bg-$prism-background);
}
.icon-btn.disabled {
@apply opacity-25 pointer-events-none;
}
</style>
99 changes: 99 additions & 0 deletions packages/vite-slides/client/builtin/WebCamera.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<script setup lang="ts">
import { useEventListener } from '@vueuse/core'
import { computed, ref, watch } from 'vue'
import { recorder } from '../logic/recording'
const size = ref(Math.round(Math.min(window.innerHeight, (window.innerWidth) / 8)))
const x = ref(window.innerWidth - size.value - 30)
const y = ref(window.innerHeight - size.value - 30)
const frame = ref<HTMLDivElement | undefined>()
const handler = ref<HTMLDivElement | undefined>()
const video = ref<HTMLVideoElement | undefined>()
const { streamCamera, showAvatar } = recorder
watch([streamCamera, video], () => {
if (video.value && streamCamera.value)
video.value.srcObject = streamCamera.value
}, { flush: 'post' })
const containerStyle = computed(() => ({
left: `${x.value}px`,
top: `${y.value}px`,
}))
const frameStyle = computed(() => ({
width: `${size.value}px`,
height: `${size.value}px`,
}))
const frameDown = ref(false)
const handlerDown = ref(false)
let deletaX = 0
let deletaY = 0
useEventListener(frame, 'mousedown', (e: MouseEvent) => {
if (frame.value) {
frameDown.value = true
const box = frame.value.getBoundingClientRect()
deletaX = e.screenX - box.x
deletaY = e.screenY - box.y
}
})
useEventListener(handler, 'mousedown', (e: MouseEvent) => {
if (frame.value) {
handlerDown.value = true
const box = frame.value.getBoundingClientRect()
deletaX = e.screenX - box.x
deletaY = e.screenY - box.y
}
})
useEventListener(window, 'mouseup', (e: MouseEvent) => {
frameDown.value = false
handlerDown.value = false
})
useEventListener(window, 'mousemove', (e: MouseEvent) => {
if (frameDown.value) {
x.value = e.screenX - deletaX
y.value = e.screenY - deletaY
}
if (handlerDown.value && frame.value) {
const box = frame.value.getBoundingClientRect()
size.value = Math.min(e.screenX - box.x, e.screenY - box.y)
}
})
</script>

<template>
<div
v-if="streamCamera && showAvatar"
class="fixed z-50"
:style="containerStyle"
>
<div
ref="frame"
class="rounded-full shadow bg-gray-400 bg-opacity-10 overflow-hidden object-cover"
:style="frameStyle"
>
<video
ref="video"
autoplay
muted
volume="0"
class="object-cover min-w-full min-h-full"
style="transform: rotateY(180deg);"
/>
</div>

<div
ref="handler"
class="absolute bottom-0 right-0 w-3 h-3 rounded-full bg-gray-400 opacity-0 shadow hover:opacity-100"
style="cursor: nwse-resize"
:class="handlerDown ? '!opacity-100' : ''"
>
</div>
</div>
</template>
117 changes: 117 additions & 0 deletions packages/vite-slides/client/logic/recording.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { Ref, ref, shallowRef, unref } from 'vue'

import Recorder from 'recordrtc'
import type { Options as RecorderOptions } from 'recordrtc'
import { MaybeRef } from '@vueuse/core'

export function useRecording() {
const recording = ref(false)
const showAvatar = ref(true)

function download(name: string, url: string) {
const a = document.createElement('a')
a.setAttribute('href', url)
a.setAttribute('download', name)
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
}

const recorderCamera: Ref<Recorder | undefined> = shallowRef()
const recorderSlides: Ref<Recorder | undefined> = shallowRef()
const streamCamera: Ref<MediaStream | undefined> = shallowRef()
const streamSlides: Ref<MediaStream | undefined> = shallowRef()

const config: RecorderOptions = {
type: 'video',
bitsPerSecond: 4 * 256 * 8 * 1024,
}

async function startRecording() {
streamCamera.value = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
})
// @ts-expect-error
streamSlides.value = await navigator.mediaDevices.getDisplayMedia({
video: {
aspectRatio: 1.6,
frameRate: 15,
width: 3840,
height: 2160,
cursor: 'motion',
resizeMode: 'crop-and-scale',
},
})
streamSlides.value!.addTrack(streamCamera.value.getAudioTracks()[0])

recorderCamera.value = new Recorder(
streamCamera.value,
config,
)
recorderSlides.value = new Recorder(
streamSlides.value!,
config,
)

recorderCamera.value.startRecording()
recorderSlides.value.startRecording()
console.log('started')
recording.value = true
}

async function stopRecording() {
recorderCamera.value?.stopRecording(() => {
const blob = recorderCamera.value!.getBlob()
const url = URL.createObjectURL(blob)
download('camera.webm', url)
window.URL.revokeObjectURL(url)
closeStream(streamCamera)
recorderCamera.value = undefined
streamCamera.value = undefined
})
recorderSlides.value?.stopRecording(() => {
const blob = recorderSlides.value!.getBlob()
const url = URL.createObjectURL(blob)
download('screen.webm', url)
window.URL.revokeObjectURL(url)
closeStream(streamSlides)
recorderSlides.value = undefined
streamSlides.value = undefined
})
recording.value = false

console.log('stopped')
}

function closeStream(stream: MaybeRef<MediaStream | undefined>) {
const s = unref(stream)
if (!s)
return
s.getTracks().forEach((i) => {
i.clone()
s.removeTrack(i)
})
}

function toggleRecording() {
if (recording.value)
stopRecording()
else
startRecording()
}

return {
recording,
showAvatar,
toggleRecording,
startRecording,
stopRecording,
recorderCamera,
recorderSlides,
streamCamera,
streamSlides,
}
}

export const recorder = useRecording()

0 comments on commit abb019b

Please sign in to comment.