Przeglądaj źródła

@uppy/audio: refactor to ESM (#3470)

npm package is still CommonJS, the plan is to switch that in the
next semver-major.
Antoine du Hamel 3 lat temu
rodzic
commit
50f5f910b1

+ 38 - 0
.eslintrc.js

@@ -169,6 +169,44 @@ module.exports = {
         },
       },
     },
+    {
+      files: [
+        // Packages that have switched to ESM sources:
+        'packages/@uppy/audio/src/**/*.js',
+        'packages/@uppy/compressor/src/**/*.js',
+      ],
+      parserOptions: {
+        sourceType: 'module',
+        ecmaFeatures: {
+          jsx: false,
+        },
+      },
+      rules: {
+        'no-restricted-globals': [
+          'error',
+          {
+            name: '__filename',
+            message: 'Use import.meta.url instead',
+          },
+          {
+            name: '__dirname',
+            message: 'Not available in ESM',
+          },
+          {
+            name: 'exports',
+            message: 'Not available in ESM',
+          },
+          {
+            name: 'module',
+            message: 'Not available in ESM',
+          },
+          {
+            name: 'require',
+            message: 'Use import instead',
+          },
+        ],
+      },
+    },
     {
       files: ['./packages/@uppy/companion/**/*.js'],
       rules: {

+ 1 - 0
packages/@uppy/audio/package.json

@@ -16,6 +16,7 @@
     "record",
     "mediarecorder"
   ],
+  "type": "module",
   "homepage": "https://uppy.io",
   "bugs": {
     "url": "https://github.com/transloadit/uppy/issues"

+ 368 - 0
packages/@uppy/audio/src/Audio.jsx

@@ -0,0 +1,368 @@
+import { h } from 'preact'
+
+import { UIPlugin } from '@uppy/core'
+
+import getFileTypeExtension from '@uppy/utils/lib/getFileTypeExtension'
+import supportsMediaRecorder from './supportsMediaRecorder.js'
+import RecordingScreen from './RecordingScreen.jsx'
+import PermissionsScreen from './PermissionsScreen.jsx'
+import locale from './locale.js'
+
+import packageJson from '../package.json'
+
+/**
+ * Audio recording plugin
+ */
+export default class Audio extends UIPlugin {
+  static VERSION = packageJson.version
+
+  #stream = null
+
+  #audioActive = false
+
+  #recordingChunks = null
+
+  #recorder = null
+
+  #capturedMediaFile = null
+
+  #mediaDevices = null
+
+  #supportsUserMedia = null
+
+  constructor (uppy, opts) {
+    super(uppy, opts)
+    this.#mediaDevices = navigator.mediaDevices
+    this.#supportsUserMedia = this.#mediaDevices != null
+    this.id = this.opts.id || 'Audio'
+    this.type = 'acquirer'
+    this.icon = () => (
+      <svg aria-hidden="true" focusable="false" width="32px" height="32px" viewBox="0 0 32 32">
+        <g fill="none" fill-rule="evenodd">
+          <rect fill="#9B59B6" width="32" height="32" rx="16" />
+          <path d="M16 20c-2.21 0-4-1.71-4-3.818V9.818C12 7.71 13.79 6 16 6s4 1.71 4 3.818v6.364C20 18.29 18.21 20 16 20zm-6.364-7h.637c.351 0 .636.29.636.65v1.95c0 3.039 2.565 5.477 5.6 5.175 2.645-.264 4.582-2.692 4.582-5.407V13.65c0-.36.285-.65.636-.65h.637c.351 0 .636.29.636.65v1.631c0 3.642-2.544 6.888-6.045 7.382v1.387h2.227c.351 0 .636.29.636.65v.65c0 .36-.285.65-.636.65h-6.364a.643.643 0 0 1-.636-.65v-.65c0-.36.285-.65.636-.65h2.227v-1.372C11.637 22.2 9 19.212 9 15.6v-1.95c0-.36.285-.65.636-.65z" fill="#FFF" fill-rule="nonzero" />
+        </g>
+      </svg>
+    )
+
+    this.defaultLocale = locale
+
+    this.opts = { ...opts }
+
+    this.i18nInit()
+    this.title = this.i18n('pluginNameAudio')
+
+    this.setPluginState({
+      hasAudio: false,
+      audioReady: false,
+      cameraError: null,
+      recordingLengthSeconds: 0,
+      audioSources: [],
+      currentDeviceId: null,
+    })
+  }
+
+  #hasAudioCheck () {
+    if (!this.#mediaDevices) {
+      return Promise.resolve(false)
+    }
+
+    return this.#mediaDevices.enumerateDevices().then(devices => {
+      return devices.some(device => device.kind === 'audioinput')
+    })
+  }
+
+  // eslint-disable-next-line consistent-return
+  #start = (options = null) => {
+    if (!this.#supportsUserMedia) {
+      return Promise.reject(new Error('Microphone access not supported'))
+    }
+
+    this.#audioActive = true
+
+    this.#hasAudioCheck().then(hasAudio => {
+      this.setPluginState({
+        hasAudio,
+      })
+
+      // ask user for access to their camera
+      return this.#mediaDevices.getUserMedia({ audio: true })
+        .then((stream) => {
+          this.#stream = stream
+
+          let currentDeviceId = null
+          const tracks = stream.getAudioTracks()
+
+          if (!options || !options.deviceId) {
+            currentDeviceId = tracks[0].getSettings().deviceId
+          } else {
+            tracks.forEach((track) => {
+              if (track.getSettings().deviceId === options.deviceId) {
+                currentDeviceId = track.getSettings().deviceId
+              }
+            })
+          }
+
+          // Update the sources now, so we can access the names.
+          this.#updateSources()
+
+          this.setPluginState({
+            currentDeviceId,
+            audioReady: true,
+          })
+        })
+        .catch((err) => {
+          this.setPluginState({
+            audioReady: false,
+            cameraError: err,
+          })
+          this.uppy.info(err.message, 'error')
+        })
+    })
+  }
+
+  #startRecording = () => {
+    // only used if supportsMediaRecorder() returned true
+    // eslint-disable-next-line compat/compat
+    this.#recorder = new MediaRecorder(this.#stream)
+    this.#recordingChunks = []
+    let stoppingBecauseOfMaxSize = false
+    this.#recorder.addEventListener('dataavailable', (event) => {
+      this.#recordingChunks.push(event.data)
+
+      const { restrictions } = this.uppy.opts
+      if (this.#recordingChunks.length > 1
+          && restrictions.maxFileSize != null
+          && !stoppingBecauseOfMaxSize) {
+        const totalSize = this.#recordingChunks.reduce((acc, chunk) => acc + chunk.size, 0)
+        // Exclude the initial chunk from the average size calculation because it is likely to be a very small outlier
+        const averageChunkSize = (totalSize - this.#recordingChunks[0].size) / (this.#recordingChunks.length - 1)
+        const expectedEndChunkSize = averageChunkSize * 3
+        const maxSize = Math.max(0, restrictions.maxFileSize - expectedEndChunkSize)
+
+        if (totalSize > maxSize) {
+          stoppingBecauseOfMaxSize = true
+          this.uppy.info(this.i18n('recordingStoppedMaxSize'), 'warning', 4000)
+          this.#stopRecording()
+        }
+      }
+    })
+
+    // use a "time slice" of 500ms: ondataavailable will be called each 500ms
+    // smaller time slices mean we can more accurately check the max file size restriction
+    this.#recorder.start(500)
+
+    // Start the recordingLengthTimer if we are showing the recording length.
+    this.recordingLengthTimer = setInterval(() => {
+      const currentRecordingLength = this.getPluginState().recordingLengthSeconds
+      this.setPluginState({ recordingLengthSeconds: currentRecordingLength + 1 })
+    }, 1000)
+
+    this.setPluginState({
+      isRecording: true,
+    })
+  }
+
+  #stopRecording = () => {
+    const stopped = new Promise((resolve) => {
+      this.#recorder.addEventListener('stop', () => {
+        resolve()
+      })
+      this.#recorder.stop()
+
+      clearInterval(this.recordingLengthTimer)
+      this.setPluginState({ recordingLengthSeconds: 0 })
+    })
+
+    return stopped.then(() => {
+      this.setPluginState({
+        isRecording: false,
+      })
+      return this.#getAudio()
+    }).then((file) => {
+      try {
+        this.#capturedMediaFile = file
+        // create object url for capture result preview
+        this.setPluginState({
+          recordedAudio: URL.createObjectURL(file.data),
+        })
+      } catch (err) {
+        // Logging the error, exept restrictions, which is handled in Core
+        if (!err.isRestriction) {
+          this.uppy.log(err)
+        }
+      }
+    }).then(() => {
+      this.#recordingChunks = null
+      this.#recorder = null
+    }, (error) => {
+      this.#recordingChunks = null
+      this.#recorder = null
+      throw error
+    })
+  }
+
+  #discardRecordedAudio = () => {
+    this.setPluginState({ recordedAudio: null })
+    this.#capturedMediaFile = null
+  }
+
+  #submit = () => {
+    try {
+      if (this.#capturedMediaFile) {
+        this.uppy.addFile(this.#capturedMediaFile)
+      }
+    } catch (err) {
+      // Logging the error, exept restrictions, which is handled in Core
+      if (!err.isRestriction) {
+        this.uppy.log(err, 'error')
+      }
+    }
+  }
+
+  #stop = async () => {
+    if (this.#stream) {
+      const audioTracks = this.#stream.getAudioTracks()
+      audioTracks.forEach((track) => track.stop())
+    }
+
+    if (this.#recorder) {
+      await new Promise((resolve) => {
+        this.#recorder.addEventListener('stop', resolve, { once: true })
+        this.#recorder.stop()
+
+        clearInterval(this.recordingLengthTimer)
+      })
+    }
+
+    this.#recordingChunks = null
+    this.#recorder = null
+    this.#audioActive = false
+    this.#stream = null
+
+    this.setPluginState({
+      recordedAudio: null,
+      isRecording: false,
+      recordingLengthSeconds: 0,
+    })
+  }
+
+  #getAudio () {
+    // Sometimes in iOS Safari, Blobs (especially the first Blob in the recordingChunks Array)
+    // have empty 'type' attributes (e.g. '') so we need to find a Blob that has a defined 'type'
+    // attribute in order to determine the correct MIME type.
+    const mimeType = this.#recordingChunks.find(blob => blob.type?.length > 0).type
+
+    const fileExtension = getFileTypeExtension(mimeType)
+
+    if (!fileExtension) {
+      return Promise.reject(new Error(`Could not retrieve recording: Unsupported media type "${mimeType}"`))
+    }
+
+    const name = `audio-${Date.now()}.${fileExtension}`
+    const blob = new Blob(this.#recordingChunks, { type: mimeType })
+    const file = {
+      source: this.id,
+      name,
+      data: new Blob([blob], { type: mimeType }),
+      type: mimeType,
+    }
+
+    return Promise.resolve(file)
+  }
+
+  #changeSource = (deviceId) => {
+    this.#stop()
+    this.#start({ deviceId })
+  }
+
+  #updateSources = () => {
+    this.#mediaDevices.enumerateDevices().then(devices => {
+      this.setPluginState({
+        audioSources: devices.filter((device) => device.kind === 'audioinput'),
+      })
+    })
+  }
+
+  render () {
+    if (!this.#audioActive) {
+      this.#start()
+    }
+
+    const audioState = this.getPluginState()
+
+    if (!audioState.audioReady || !audioState.hasAudio) {
+      return (
+        <PermissionsScreen
+          icon={this.icon}
+          i18n={this.i18n}
+          hasAudio={audioState.hasAudio}
+        />
+      )
+    }
+
+    return (
+      <RecordingScreen
+        // eslint-disable-next-line react/jsx-props-no-spreading
+        {...audioState}
+        audioActive={this.#audioActive}
+        onChangeSource={this.#changeSource}
+        onStartRecording={this.#startRecording}
+        onStopRecording={this.#stopRecording}
+        onDiscardRecordedAudio={this.#discardRecordedAudio}
+        onSubmit={this.#submit}
+        onStop={this.#stop}
+        i18n={this.i18n}
+        showAudioSourceDropdown={this.opts.showAudioSourceDropdown}
+        supportsRecording={supportsMediaRecorder()}
+        recording={audioState.isRecording}
+        stream={this.#stream}
+      />
+    )
+  }
+
+  install () {
+    this.setPluginState({
+      audioReady: false,
+      recordingLengthSeconds: 0,
+    })
+
+    const { target } = this.opts
+    if (target) {
+      this.mount(target, this)
+    }
+
+    if (this.#mediaDevices) {
+      this.#updateSources()
+
+      this.#mediaDevices.ondevicechange = () => {
+        this.#updateSources()
+
+        if (this.#stream) {
+          let restartStream = true
+
+          const { audioSources, currentDeviceId } = this.getPluginState()
+
+          audioSources.forEach((audioSource) => {
+            if (currentDeviceId === audioSource.deviceId) {
+              restartStream = false
+            }
+          })
+
+          if (restartStream) {
+            this.#stop()
+            this.#start()
+          }
+        }
+      }
+    }
+  }
+
+  uninstall () {
+    if (this.#stream) {
+      this.#stop()
+    }
+
+    this.unmount()
+  }
+}

+ 2 - 2
packages/@uppy/audio/src/AudioSourceSelect.js → packages/@uppy/audio/src/AudioSourceSelect.jsx

@@ -1,6 +1,6 @@
-const { h } = require('preact')
+import { h } from 'preact'
 
-module.exports = ({ currentDeviceId, audioSources, onChangeSource }) => {
+export default ({ currentDeviceId, audioSources, onChangeSource }) => {
   return (
     <div className="uppy-Audio-videoSource">
       <select

+ 2 - 2
packages/@uppy/audio/src/DiscardButton.js → packages/@uppy/audio/src/DiscardButton.jsx

@@ -1,4 +1,4 @@
-const { h } = require('preact')
+import { h } from 'preact'
 
 function DiscardButton ({ onDiscard, i18n }) {
   return (
@@ -27,4 +27,4 @@ function DiscardButton ({ onDiscard, i18n }) {
   )
 }
 
-module.exports = DiscardButton
+export default DiscardButton

+ 2 - 2
packages/@uppy/audio/src/PermissionsScreen.js → packages/@uppy/audio/src/PermissionsScreen.jsx

@@ -1,6 +1,6 @@
-const { h } = require('preact')
+import { h } from 'preact'
 
-module.exports = (props) => {
+export default (props) => {
   const { icon, hasAudio, i18n } = props
   return (
     <div className="uppy-Audio-permissons">

+ 2 - 2
packages/@uppy/audio/src/RecordButton.js → packages/@uppy/audio/src/RecordButton.jsx

@@ -1,6 +1,6 @@
-const { h } = require('preact')
+import { h } from 'preact'
 
-module.exports = function RecordButton ({ recording, onStartRecording, onStopRecording, i18n }) {
+export default function RecordButton ({ recording, onStartRecording, onStopRecording, i18n }) {
   if (recording) {
     return (
       <button

+ 3 - 3
packages/@uppy/audio/src/RecordingLength.js → packages/@uppy/audio/src/RecordingLength.jsx

@@ -1,7 +1,7 @@
-const { h } = require('preact')
-const formatSeconds = require('./formatSeconds')
+import { h } from 'preact'
+import formatSeconds from './formatSeconds.js'
 
-module.exports = function RecordingLength ({ recordingLengthSeconds, i18n }) {
+export default function RecordingLength ({ recordingLengthSeconds, i18n }) {
   const formattedRecordingLengthSeconds = formatSeconds(recordingLengthSeconds)
 
   return (

+ 9 - 9
packages/@uppy/audio/src/RecordingScreen.js → packages/@uppy/audio/src/RecordingScreen.jsx

@@ -1,14 +1,14 @@
 /* eslint-disable jsx-a11y/media-has-caption */
-const { h } = require('preact')
-const { useEffect, useRef } = require('preact/hooks')
-const RecordButton = require('./RecordButton')
-const RecordingLength = require('./RecordingLength')
-const AudioSourceSelect = require('./AudioSourceSelect')
-const AudioOscilloscope = require('./audio-oscilloscope')
-const SubmitButton = require('./SubmitButton')
-const DiscardButton = require('./DiscardButton')
+import { h } from 'preact'
+import { useEffect, useRef } from 'preact/hooks'
+import RecordButton from './RecordButton.jsx'
+import RecordingLength from './RecordingLength.jsx'
+import AudioSourceSelect from './AudioSourceSelect.jsx'
+import AudioOscilloscope from './audio-oscilloscope/index.js'
+import SubmitButton from './SubmitButton.jsx'
+import DiscardButton from './DiscardButton.jsx'
 
-module.exports = function RecordingScreen (props) {
+export default function RecordingScreen (props) {
   const {
     stream,
     recordedAudio,

+ 2 - 2
packages/@uppy/audio/src/SubmitButton.js → packages/@uppy/audio/src/SubmitButton.jsx

@@ -1,4 +1,4 @@
-const { h } = require('preact')
+import { h } from 'preact'
 
 function SubmitButton ({ onSubmit, i18n }) {
   return (
@@ -25,4 +25,4 @@ function SubmitButton ({ onSubmit, i18n }) {
   )
 }
 
-module.exports = SubmitButton
+export default SubmitButton

+ 1 - 1
packages/@uppy/audio/src/audio-oscilloscope/index.js

@@ -9,7 +9,7 @@ function result (v) {
 /* Audio Oscilloscope
   https://github.com/miguelmota/audio-oscilloscope
 */
-module.exports = class AudioOscilloscope {
+export default class AudioOscilloscope {
   constructor (canvas, options = {}) {
     const canvasOptions = options.canvas || {}
     const canvasContextOptions = options.canvasContext || {}

+ 1 - 1
packages/@uppy/audio/src/formatSeconds.js

@@ -5,7 +5,7 @@
  * @returns {string} the formatted seconds (e.g. '1:23' for 1 minute and 23 seconds)
  *
  */
-module.exports = function formatSeconds (seconds) {
+export default function formatSeconds (seconds) {
   return `${Math.floor(
     seconds / 60,
   )}:${String(seconds % 60).padStart(2, 0)}`

+ 1 - 1
packages/@uppy/audio/src/formatSeconds.test.js

@@ -1,4 +1,4 @@
-const formatSeconds = require('./formatSeconds')
+import formatSeconds from './formatSeconds.js'
 
 describe('formatSeconds', () => {
   it('should return a value of \'0:43\' when an argument of 43 seconds is supplied', () => {

+ 1 - 364
packages/@uppy/audio/src/index.js

@@ -1,364 +1 @@
-const { h } = require('preact')
-const { UIPlugin } = require('@uppy/core')
-const getFileTypeExtension = require('@uppy/utils/lib/getFileTypeExtension')
-const supportsMediaRecorder = require('./supportsMediaRecorder')
-const RecordingScreen = require('./RecordingScreen')
-const PermissionsScreen = require('./PermissionsScreen')
-const locale = require('./locale.js')
-
-/**
- * Audio recording plugin
- */
-module.exports = class Audio extends UIPlugin {
-  static VERSION = require('../package.json').version
-
-  #stream = null
-
-  #audioActive = false
-
-  #recordingChunks = null
-
-  #recorder = null
-
-  #capturedMediaFile = null
-
-  #mediaDevices = null
-
-  #supportsUserMedia = null
-
-  constructor (uppy, opts) {
-    super(uppy, opts)
-    this.#mediaDevices = navigator.mediaDevices
-    this.#supportsUserMedia = this.#mediaDevices != null
-    this.id = this.opts.id || 'Audio'
-    this.type = 'acquirer'
-    this.icon = () => (
-      <svg aria-hidden="true" focusable="false" width="32px" height="32px" viewBox="0 0 32 32">
-        <g fill="none" fill-rule="evenodd">
-          <rect fill="#9B59B6" width="32" height="32" rx="16" />
-          <path d="M16 20c-2.21 0-4-1.71-4-3.818V9.818C12 7.71 13.79 6 16 6s4 1.71 4 3.818v6.364C20 18.29 18.21 20 16 20zm-6.364-7h.637c.351 0 .636.29.636.65v1.95c0 3.039 2.565 5.477 5.6 5.175 2.645-.264 4.582-2.692 4.582-5.407V13.65c0-.36.285-.65.636-.65h.637c.351 0 .636.29.636.65v1.631c0 3.642-2.544 6.888-6.045 7.382v1.387h2.227c.351 0 .636.29.636.65v.65c0 .36-.285.65-.636.65h-6.364a.643.643 0 0 1-.636-.65v-.65c0-.36.285-.65.636-.65h2.227v-1.372C11.637 22.2 9 19.212 9 15.6v-1.95c0-.36.285-.65.636-.65z" fill="#FFF" fill-rule="nonzero" />
-        </g>
-      </svg>
-    )
-
-    this.defaultLocale = locale
-
-    this.opts = { ...opts }
-
-    this.i18nInit()
-    this.title = this.i18n('pluginNameAudio')
-
-    this.setPluginState({
-      hasAudio: false,
-      audioReady: false,
-      cameraError: null,
-      recordingLengthSeconds: 0,
-      audioSources: [],
-      currentDeviceId: null,
-    })
-  }
-
-  #hasAudioCheck () {
-    if (!this.#mediaDevices) {
-      return Promise.resolve(false)
-    }
-
-    return this.#mediaDevices.enumerateDevices().then(devices => {
-      return devices.some(device => device.kind === 'audioinput')
-    })
-  }
-
-  // eslint-disable-next-line consistent-return
-  #start = (options = null) => {
-    if (!this.#supportsUserMedia) {
-      return Promise.reject(new Error('Microphone access not supported'))
-    }
-
-    this.#audioActive = true
-
-    this.#hasAudioCheck().then(hasAudio => {
-      this.setPluginState({
-        hasAudio,
-      })
-
-      // ask user for access to their camera
-      return this.#mediaDevices.getUserMedia({ audio: true })
-        .then((stream) => {
-          this.#stream = stream
-
-          let currentDeviceId = null
-          const tracks = stream.getAudioTracks()
-
-          if (!options || !options.deviceId) {
-            currentDeviceId = tracks[0].getSettings().deviceId
-          } else {
-            tracks.forEach((track) => {
-              if (track.getSettings().deviceId === options.deviceId) {
-                currentDeviceId = track.getSettings().deviceId
-              }
-            })
-          }
-
-          // Update the sources now, so we can access the names.
-          this.#updateSources()
-
-          this.setPluginState({
-            currentDeviceId,
-            audioReady: true,
-          })
-        })
-        .catch((err) => {
-          this.setPluginState({
-            audioReady: false,
-            cameraError: err,
-          })
-          this.uppy.info(err.message, 'error')
-        })
-    })
-  }
-
-  #startRecording = () => {
-    // only used if supportsMediaRecorder() returned true
-    // eslint-disable-next-line compat/compat
-    this.#recorder = new MediaRecorder(this.#stream)
-    this.#recordingChunks = []
-    let stoppingBecauseOfMaxSize = false
-    this.#recorder.addEventListener('dataavailable', (event) => {
-      this.#recordingChunks.push(event.data)
-
-      const { restrictions } = this.uppy.opts
-      if (this.#recordingChunks.length > 1
-          && restrictions.maxFileSize != null
-          && !stoppingBecauseOfMaxSize) {
-        const totalSize = this.#recordingChunks.reduce((acc, chunk) => acc + chunk.size, 0)
-        // Exclude the initial chunk from the average size calculation because it is likely to be a very small outlier
-        const averageChunkSize = (totalSize - this.#recordingChunks[0].size) / (this.#recordingChunks.length - 1)
-        const expectedEndChunkSize = averageChunkSize * 3
-        const maxSize = Math.max(0, restrictions.maxFileSize - expectedEndChunkSize)
-
-        if (totalSize > maxSize) {
-          stoppingBecauseOfMaxSize = true
-          this.uppy.info(this.i18n('recordingStoppedMaxSize'), 'warning', 4000)
-          this.#stopRecording()
-        }
-      }
-    })
-
-    // use a "time slice" of 500ms: ondataavailable will be called each 500ms
-    // smaller time slices mean we can more accurately check the max file size restriction
-    this.#recorder.start(500)
-
-    // Start the recordingLengthTimer if we are showing the recording length.
-    this.recordingLengthTimer = setInterval(() => {
-      const currentRecordingLength = this.getPluginState().recordingLengthSeconds
-      this.setPluginState({ recordingLengthSeconds: currentRecordingLength + 1 })
-    }, 1000)
-
-    this.setPluginState({
-      isRecording: true,
-    })
-  }
-
-  #stopRecording = () => {
-    const stopped = new Promise((resolve) => {
-      this.#recorder.addEventListener('stop', () => {
-        resolve()
-      })
-      this.#recorder.stop()
-
-      clearInterval(this.recordingLengthTimer)
-      this.setPluginState({ recordingLengthSeconds: 0 })
-    })
-
-    return stopped.then(() => {
-      this.setPluginState({
-        isRecording: false,
-      })
-      return this.#getAudio()
-    }).then((file) => {
-      try {
-        this.#capturedMediaFile = file
-        // create object url for capture result preview
-        this.setPluginState({
-          recordedAudio: URL.createObjectURL(file.data),
-        })
-      } catch (err) {
-        // Logging the error, exept restrictions, which is handled in Core
-        if (!err.isRestriction) {
-          this.uppy.log(err)
-        }
-      }
-    }).then(() => {
-      this.#recordingChunks = null
-      this.#recorder = null
-    }, (error) => {
-      this.#recordingChunks = null
-      this.#recorder = null
-      throw error
-    })
-  }
-
-  #discardRecordedAudio = () => {
-    this.setPluginState({ recordedAudio: null })
-    this.#capturedMediaFile = null
-  }
-
-  #submit = () => {
-    try {
-      if (this.#capturedMediaFile) {
-        this.uppy.addFile(this.#capturedMediaFile)
-      }
-    } catch (err) {
-      // Logging the error, exept restrictions, which is handled in Core
-      if (!err.isRestriction) {
-        this.uppy.log(err, 'error')
-      }
-    }
-  }
-
-  #stop = async () => {
-    if (this.#stream) {
-      const audioTracks = this.#stream.getAudioTracks()
-      audioTracks.forEach((track) => track.stop())
-    }
-
-    if (this.#recorder) {
-      await new Promise((resolve) => {
-        this.#recorder.addEventListener('stop', resolve, { once: true })
-        this.#recorder.stop()
-
-        clearInterval(this.recordingLengthTimer)
-      })
-    }
-
-    this.#recordingChunks = null
-    this.#recorder = null
-    this.#audioActive = false
-    this.#stream = null
-
-    this.setPluginState({
-      recordedAudio: null,
-      isRecording: false,
-      recordingLengthSeconds: 0,
-    })
-  }
-
-  #getAudio () {
-    // Sometimes in iOS Safari, Blobs (especially the first Blob in the recordingChunks Array)
-    // have empty 'type' attributes (e.g. '') so we need to find a Blob that has a defined 'type'
-    // attribute in order to determine the correct MIME type.
-    const mimeType = this.#recordingChunks.find(blob => blob.type?.length > 0).type
-
-    const fileExtension = getFileTypeExtension(mimeType)
-
-    if (!fileExtension) {
-      return Promise.reject(new Error(`Could not retrieve recording: Unsupported media type "${mimeType}"`))
-    }
-
-    const name = `audio-${Date.now()}.${fileExtension}`
-    const blob = new Blob(this.#recordingChunks, { type: mimeType })
-    const file = {
-      source: this.id,
-      name,
-      data: new Blob([blob], { type: mimeType }),
-      type: mimeType,
-    }
-
-    return Promise.resolve(file)
-  }
-
-  #changeSource = (deviceId) => {
-    this.#stop()
-    this.#start({ deviceId })
-  }
-
-  #updateSources = () => {
-    this.#mediaDevices.enumerateDevices().then(devices => {
-      this.setPluginState({
-        audioSources: devices.filter((device) => device.kind === 'audioinput'),
-      })
-    })
-  }
-
-  render () {
-    if (!this.#audioActive) {
-      this.#start()
-    }
-
-    const audioState = this.getPluginState()
-
-    if (!audioState.audioReady || !audioState.hasAudio) {
-      return (
-        <PermissionsScreen
-          icon={this.icon}
-          i18n={this.i18n}
-          hasAudio={audioState.hasAudio}
-        />
-      )
-    }
-
-    return (
-      <RecordingScreen
-        // eslint-disable-next-line react/jsx-props-no-spreading
-        {...audioState}
-        audioActive={this.#audioActive}
-        onChangeSource={this.#changeSource}
-        onStartRecording={this.#startRecording}
-        onStopRecording={this.#stopRecording}
-        onDiscardRecordedAudio={this.#discardRecordedAudio}
-        onSubmit={this.#submit}
-        onStop={this.#stop}
-        i18n={this.i18n}
-        showAudioSourceDropdown={this.opts.showAudioSourceDropdown}
-        supportsRecording={supportsMediaRecorder()}
-        recording={audioState.isRecording}
-        stream={this.#stream}
-      />
-    )
-  }
-
-  install () {
-    this.setPluginState({
-      audioReady: false,
-      recordingLengthSeconds: 0,
-    })
-
-    const { target } = this.opts
-    if (target) {
-      this.mount(target, this)
-    }
-
-    if (this.#mediaDevices) {
-      this.#updateSources()
-
-      this.#mediaDevices.ondevicechange = () => {
-        this.#updateSources()
-
-        if (this.#stream) {
-          let restartStream = true
-
-          const { audioSources, currentDeviceId } = this.getPluginState()
-
-          audioSources.forEach((audioSource) => {
-            if (currentDeviceId === audioSource.deviceId) {
-              restartStream = false
-            }
-          })
-
-          if (restartStream) {
-            this.#stop()
-            this.#start()
-          }
-        }
-      }
-    }
-  }
-
-  uninstall () {
-    if (this.#stream) {
-      this.#stop()
-    }
-
-    this.unmount()
-  }
-}
+export { default } from './Audio.jsx'

+ 1 - 1
packages/@uppy/audio/src/locale.js

@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   strings: {
     pluginNameAudio: 'Audio',
     // Used as the label for the button that starts an audio recording.

+ 1 - 1
packages/@uppy/audio/src/supportsMediaRecorder.js

@@ -1,4 +1,4 @@
-module.exports = function supportsMediaRecorder () {
+export default function supportsMediaRecorder () {
   /* eslint-disable compat/compat */
   return typeof MediaRecorder === 'function'
     && typeof MediaRecorder.prototype?.start === 'function'

+ 2 - 1
packages/@uppy/audio/src/supportsMediaRecorder.test.js

@@ -1,4 +1,5 @@
-const supportsMediaRecorder = require('./supportsMediaRecorder')
+/* eslint-disable max-classes-per-file */
+import supportsMediaRecorder from './supportsMediaRecorder.js'
 
 describe('supportsMediaRecorder', () => {
   it('should return true if MediaRecorder is supported', () => {