Skip to content

Commit

Permalink
feat: rework state and mimetype computed
Browse files Browse the repository at this point in the history
  • Loading branch information
OrbisK committed Nov 27, 2024
1 parent eb51ce0 commit 490923a
Show file tree
Hide file tree
Showing 4 changed files with 40 additions and 27 deletions.
5 changes: 3 additions & 2 deletions playgrounds/nuxt/app/components/recorder-demo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@ const { start, stop, pause, resume, data, state, isMimeTypeSupported, isSupporte
},
})
function handleStop() {
console.log('stop')
stop()
const blob = new Blob(data.value)
const blobVideo = new Blob(data.value)
audio.value.src = URL.createObjectURL(blob)
video.value.src = URL.createObjectURL(blobVideo)
data.value = []
}
</script>

<template>
<div>
<button @click="start()">
<button @click="start">
start
</button>
<button @click="pause">
Expand All @@ -37,5 +37,6 @@ function handleStop() {
<pre>supported: {{ isSupported }}</pre>
<pre>mime type: {{ mimeType }}</pre>
<pre>mime supported: {{ isMimeTypeSupported }}</pre>
<pre>data length: {{ data?.length}}</pre>
</div>
</template>
6 changes: 5 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
// Components
// export { default as Toggle } from './components/Toggle.vue'

// Plugin
export { default as MediaRecorderPlugin } from './plugin'

// Composables
export { useMediaRecorder } from './useMediaRecorder'

// Types
// export * from './types'
export type {MediaRecorderPluginOptions} from './types'
export type {UseMediaRecorderReturn} from './useMediaRecorder'
47 changes: 24 additions & 23 deletions src/useMediaRecorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import { useSupported } from '@vueuse/core'
import { tryOnScopeDispose } from '@vueuse/shared'
import { defu } from 'defu'
import { computed, ref, shallowRef, toValue, watch } from 'vue'

export { MediaRecorderPlugin } from './plugin'
import { computedWithControl } from '@vueuse/core'

interface UseMediaRecorderOptions extends ConfigurableNavigator {
/**
Expand All @@ -20,13 +19,14 @@ interface UseMediaRecorderOptions extends ConfigurableNavigator {

const defaultOptions: UseMediaRecorderOptions = {
constraints: { audio: false, video: false },
mediaRecorderOptions: {},
mediaRecorderOptions: {}
}

export function useMediaRecorder(options: UseMediaRecorderOptions = {}) {
const data = ref<Blob[]>([])
const mediaRecorder = shallowRef<MediaRecorder>()
const stream = shallowRef<MediaStream>()
const result = shallowRef<Blob[]>([])

const isMimeTypeSupported = computed(() => {
return toValue(options.mediaRecorderOptions)?.mimeType ? MediaRecorder.isTypeSupported(toValue(options.mediaRecorderOptions)?.mimeType ?? '') : true
Expand All @@ -35,66 +35,67 @@ export function useMediaRecorder(options: UseMediaRecorderOptions = {}) {
return !!navigator?.mediaDevices?.getUserMedia && isMimeTypeSupported.value
})

const state = shallowRef<RecordingState | undefined>(undefined)
const state = computedWithControl<RecordingState | undefined>(()=>mediaRecorder.value,()=>{
return mediaRecorder.value?.state
})

const mimeType = shallowRef<string | undefined>(undefined)
const mimeType = computedWithControl<string | undefined>(() => mediaRecorder.value, () => {
return mediaRecorder.value?.mimeType
})

const updateStates = () => {
state.value = mediaRecorder.value?.state
state.trigger()
}

const {
mediaRecorderOptions,
constraints,
constraints
} = defu(options, defaultOptions)

const start = async (timeslice: number | undefined = undefined) => {
if (state.value === 'recording')
return // todo warning?
data.value = []
stream.value = await navigator!.mediaDevices.getUserMedia(toValue(constraints))
mediaRecorder.value = new MediaRecorder(stream.value, toValue(mediaRecorderOptions))
data.value = []
mediaRecorder.value?.start(timeslice)
updateStates()
}

const reset = () => {
stream.value?.getTracks().forEach(t => t.stop())
stream.value = undefined
mediaRecorder.value?.stop()
}

const stop = () => {
if (!state.value || state.value === 'inactive')
return // todo warning?
reset()
updateStates()
mediaRecorder.value?.stop()
}

const pause = () => {
if (state.value !== 'recording')
return // todo warning?
mediaRecorder.value?.pause()
updateStates()
}

const resume = () => {
if (state.value !== 'paused')
return // todo warning?
mediaRecorder.value?.resume()
updateStates()
}

watch(() => mediaRecorder.value, (newMediaRecorder) => {
if (!newMediaRecorder)
return
newMediaRecorder.ondataavailable = (e) => {
const blob = e.data
if (blob.type !== mimeType.value) {
mimeType.value = blob.type ?? mediaRecorder.value?.mimeType
}
mimeType.trigger()
data.value.push(e.data)
}
newMediaRecorder.onstop = () => {
stream.value?.getTracks().forEach(t => t.stop())
result.value = data.value
updateStates()
}
newMediaRecorder.onpause = updateStates
newMediaRecorder.onresume = updateStates
newMediaRecorder.onstart = updateStates
newMediaRecorder.onerror = updateStates
}, { immediate: true })

tryOnScopeDispose(() => {
Expand All @@ -112,7 +113,7 @@ export function useMediaRecorder(options: UseMediaRecorderOptions = {}) {
isSupported,
isMimeTypeSupported,
mimeType: computed(()=> mimeType.value),
mediaRecorder: computed(() => mediaRecorder.value),
mediaRecorder: computed(() => mediaRecorder.value)
}
}

Expand Down
9 changes: 8 additions & 1 deletion tests/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ describe('useMediaRecorder', () => {
resume,
} = useMediaRecorder({ constraints: { audio: true } })
await start()
await new Promise(resolve => setTimeout(resolve, 10))
expect(state.value).toMatchInlineSnapshot(`"recording"`)
await pause()
await new Promise(resolve => setTimeout(resolve, 10))
expect(state.value).toMatchInlineSnapshot(`"paused"`)
await resume()
await new Promise(resolve => setTimeout(resolve, 10))
expect(state.value).toMatchInlineSnapshot(`"recording"`)
await stop()
await new Promise(resolve => setTimeout(resolve, 10))
expect(state.value).toMatchInlineSnapshot(`"inactive"`)
})

Expand Down Expand Up @@ -57,7 +61,7 @@ describe('useMediaRecorder', () => {
} = useMediaRecorder({ constraints: { audio: true } })
await start()
await stop()
expect(stream.value).toBeUndefined()
expect(stream.value.active).toMatchInlineSnapshot(`true`)
})

it('stream should be undefined after pause', async () => {
Expand Down Expand Up @@ -120,10 +124,13 @@ describe('useMediaRecorder', () => {
} = useMediaRecorder({ constraints: { audio: true } })

await start()
await new Promise(resolve => setTimeout(resolve, 10))
expect(state.value).toBe('recording')
await pause()
await new Promise(resolve => setTimeout(resolve, 10))
expect(state.value).toBe('paused')
await stop()
await new Promise(resolve => setTimeout(resolve, 10))
expect(state.value).toBe('inactive')
})

Expand Down

0 comments on commit 490923a

Please sign in to comment.