Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix recording settings (define constraints) #30

Merged
merged 1 commit into from
Dec 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { useKeybindings } from '../hooks/use-keybindings'
import { KeyboardBindingsList } from './KeyboardBindingsList'

type Props = {
stream: MediaStream
defaultDeviceId: string
audioContext: AudioContext
devices: MediaDeviceInfo[]
}

function App(props: Props) {
Expand Down
6 changes: 4 additions & 2 deletions src/AudioProvider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ import React, { createContext, useContext } from 'react'

type AudioAdapter = {
audioContext: AudioContext
stream: MediaStream
defaultDeviceId: string
devices: MediaDeviceInfo[]
}

const AudioRouter = createContext<AudioAdapter | null>(null)

type Props = {
stream: MediaStream
defaultDeviceId: string
audioContext: AudioContext
devices: MediaDeviceInfo[]
children: React.ReactNode
}

Expand Down
67 changes: 51 additions & 16 deletions src/Start/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
*/
import { useEffect, useState } from 'react'
import App from '../App'
import { deviceIdFromStream } from '../util/device-id-from-stream'
import { logger } from '../util/logger'

export const Start: React.FC = () => {
const [stream, setStream] = useState<MediaStream>()
const [defaultDeviceId, setDefaultDeviceId] = useState<string | null>(null)
const [audioContext, setAudioContext] = useState<AudioContext>()
const [devices, setDevices] = useState<MediaDeviceInfo[] | null>(null)
const [latencySupported, setLatencySupported] = useState(true)

useEffect(() => {
Expand All @@ -33,27 +35,48 @@ export const Start: React.FC = () => {
}
}, [])

async function handleClick() {
async function grantDevicePermission() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
// see src/Track/controls/SelectInput.tsx for related note
audio: {
echoCancellation: false,
autoGainControl: false,
noiseSuppression: false,
suppressLocalAudioPlayback: false,
latency: 0,
},
video: false,
})
setDefaultDeviceId(deviceIdFromStream(stream) ?? null)
} catch (e) {
alert(
'big, terrible error occurred and there is no coming back from that 😿'
)
logger.error({ e, message: 'Error getting user media' })
}
}

async function enumerateDevices() {
try {
setStream(
await navigator.mediaDevices.getUserMedia({
audio: true,
// audio: {
// echoCancellation: true,
// autoGainControl: false,
// noiseSuppression: true,
// latency: 0,
// },
video: false,
})
const devices = await navigator.mediaDevices.enumerateDevices()
const audioDevices = devices.filter(
(device) => device.kind === 'audioinput'
)
if (audioDevices.length === 0) {
logger.error({ devices })
throw new Error('No audio devices found')
}
setDevices(audioDevices)
} catch (e) {
alert(
'big, terrible error occurred and there is no coming back from that 😿'
)
logger.error(e, 'Error getting user media')
logger.error({ e, message: 'Error getting user devices' })
}
}

async function initializeAudioContext() {
const workletUrl = new URL('../workers/recorder', import.meta.url)
try {
const audioContext = new AudioContext()
Expand All @@ -71,8 +94,20 @@ export const Start: React.FC = () => {
}
}

return stream && audioContext ? (
<App stream={stream} audioContext={audioContext} />
async function handleClick() {
await Promise.all([
grantDevicePermission(),
enumerateDevices(),
initializeAudioContext(),
])
}

return defaultDeviceId && audioContext && devices?.length ? (
<App
defaultDeviceId={defaultDeviceId}
audioContext={audioContext}
devices={devices}
/>
) : (
<>
<div className="flex flex-col items-center justify-center mx-auto">
Expand Down
73 changes: 43 additions & 30 deletions src/Track/controls/SelectInput.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,68 @@
import { useEffect, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { logger } from '../../util/logger'
import { deviceIdFromStream } from '../../util/device-id-from-stream'
import { useAudioContext } from '../../AudioProvider'

type Props = {
defaultDeviceId: string
setStream(stream: MediaStream): void
}

export function SelectInput(props: Props) {
const [inputs, setInputs] = useState<MediaDeviceInfo[]>([])
const [selected, setSelected] = useState(props.defaultDeviceId)
export function SelectInput({ setStream }: Props) {
const { devices, defaultDeviceId } = useAudioContext()
const [selected, setSelected] = useState(defaultDeviceId)

useEffect(() => {
async function getInputs() {
const devices = await navigator.mediaDevices.enumerateDevices()
const audioInputs = devices.filter(
(device) => device.kind === 'audioinput'
)
logger.debug({ audioInputs })
setInputs(audioInputs)
}
const setStreamByDeviceId = useCallback(
async (id: string) => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
deviceId: id,

getInputs()
}, [])
// for some reason,
// having these defined makes a HUGE difference in the recording quality.
// Without these defined, the audio will pulse in and out, but with these defined, it sounds great
echoCancellation: false,
autoGainControl: false,
noiseSuppression: false,
suppressLocalAudioPlayback: false,
latency: 0,
},
video: false,
})
setStream(stream)
setSelected(deviceIdFromStream(stream) ?? '')
} catch (e) {
alert('oh no, you broke it 😿')
logger.error({
e,
id,
message: 'Failed to create stream from selected device',
})
}
},
[setStream]
)

const handleChange: React.ChangeEventHandler<HTMLSelectElement> = async (
event
) => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: { deviceId: event.target.value },
video: false,
})
props.setStream(stream)
setSelected(deviceIdFromStream(stream) ?? '')
} catch (e) {
alert('oh no, you broke it 😿')
console.error(e)
}
return setStreamByDeviceId(event.target.value)
}

useEffect(() => {
setStreamByDeviceId(defaultDeviceId)
}, [setStreamByDeviceId, defaultDeviceId])

return (
<select
className="rounded-full bg-white border border-zinc-400 px-2 max-w-[50%] text-xs"
onChange={handleChange}
value={selected}
>
{inputs.map((input) => (
<option key={JSON.stringify(input)} value={input.deviceId}>
{devices.map((device) => (
<option key={JSON.stringify(device)} value={device.deviceId}>
{/* Chrome appends a weird hex ID to some inputs */}
{input.label.replace(/\([a-z0-9]+:[a-z0-9]+\)/, '')}
{device.label.replace(/\([a-z0-9]+:[a-z0-9]+\)/, '')}
</option>
))}
</select>
Expand Down
15 changes: 7 additions & 8 deletions src/Track/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import { Mute } from './controls/Mute'
import { RemoveTrack } from './controls/RemoveTrack'
import { Waveform } from './Waveform'
import { SelectInput } from './controls/SelectInput'
import { deviceIdFromStream } from '../util/device-id-from-stream'
import type {
ExportWavWorkerEvent,
WavBlobControllerEvent,
Expand Down Expand Up @@ -84,9 +83,9 @@ export const Track: React.FC<Props> = ({
selected,
exportTarget,
}) => {
const { audioContext, stream: defaultStream } = useAudioContext()
const [stream, setStream] = useState(defaultStream)
const defaultDeviceId = deviceIdFromStream(defaultStream) ?? ''
const { audioContext } = useAudioContext()
// stream is initialized in SelectInput
const [stream, setStream] = useState<MediaStream | null>(null)

// title is mostly display-only, but also defines the file name when downloading files
const [title, setTitle] = useState(`Track ${id}`)
Expand Down Expand Up @@ -229,6 +228,9 @@ export const Track: React.FC<Props> = ({
* Initialize the recorder worklet, and connect the audio graph for eventual playback.
*/
useEffect(() => {
if (!stream) {
return
}
const mediaSource = audioContext.createMediaStreamSource(stream)
const recordingProperties: RecordingProperties = {
numberOfChannels: mediaSource.channelCount,
Expand Down Expand Up @@ -424,10 +426,7 @@ export const Track: React.FC<Props> = ({
{/* Remove */}
<div className="flex items-stretch content-center justify-between">
<RemoveTrack onRemove={onRemove} />
<SelectInput
setStream={setStream}
defaultDeviceId={defaultDeviceId}
/>
<SelectInput setStream={setStream} />
</div>
</div>

Expand Down