JavaScript - 如何立即回放录制的音频

时间:2018-04-17 19:54:33

标签: javascript html5-audio web-mediarecorder

我已经构建了一个非常简单的POC,用于使用麦克风录制音频文件,并将录制的blob附加到浏览器中的audio标签元素中。问题是录制完成后,我无法来回回放,直到录制完全加载。看起来持续时间有问题。我想要实现的是这样的:

https://online-voice-recorder.com/beta/

就在那里,完成录制后,即使长度为30分钟,您也可以立即回放到录制结束。它就像魔法一样。怎么能实现这一目标?

这是我编写的代码(主要是从MDN复制的)。您可以复制粘贴到任何index.html:

<body>
    <button class="record">RECORD</button>
    <button class="stop">STOP</button>
    <div class="clips"></div>
    <script>
    if (navigator.mediaDevices) {
        const record = document.querySelector('.record')
        const stop = document.querySelector('.stop')
        const soundClips = document.querySelector('.clips')

        const constraints = { audio: true };
        let chunks = [];

        navigator.mediaDevices.getUserMedia(constraints)
            .then(function (stream) {

                const mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });

                record.onclick = function () {
                    mediaRecorder.start();
                    record.style.background = "red";
                    record.style.color = "black";
                }

                stop.onclick = function () {
                    mediaRecorder.stop();
                    record.style.background = "";
                    record.style.color = "";
                }

                mediaRecorder.onstop = function (e) {
                    const clipName = prompt('Enter a name for your sound clip');

                    const clipContainer = document.createElement('article');
                    const clipLabel = document.createElement('p');
                    const audio = document.createElement('audio');
                    const deleteButton = document.createElement('button');

                    clipContainer.classList.add('clip');
                    audio.setAttribute('controls', '');
                    audio.setAttribute('preload', 'metadata');
                    deleteButton.innerHTML = "Delete";
                    clipLabel.innerHTML = clipName;

                    clipContainer.appendChild(audio);
                    clipContainer.appendChild(clipLabel);
                    clipContainer.appendChild(deleteButton);
                    soundClips.appendChild(clipContainer);

                    audio.controls = true;
                    const blob = new Blob(chunks);
                    chunks = [];
                    const audioURL = URL.createObjectURL(blob);
                    audio.src = audioURL;

                    deleteButton.onclick = function (e) {
                        evtTgt = e.target;
                        evtTgt.parentNode.parentNode.removeChild(evtTgt.parentNode);
                    }
                }

                mediaRecorder.ondataavailable = function (e) {
                    chunks.push(e.data);
                }
            })
            .catch(function (err) {
                console.log('The following error occurred: ' + err);
            })
    }
    </script>
</body>

3 个答案:

答案 0 :(得分:1)

在您点击dataURL之前,您所拥有的代码似乎无法创建stop()。因此,如果您记录30分钟,则可能需要一段时间才能解析所有内容。

相反,您可以做的是按照您的方式构建URL,并随时重建新的URL。这样,当你点击停止时,URL基本上已经完成了最后一个缺少最后一个x秒的版本,并且当最后一个版本构建完成后,你将它们交换出来并将它们放在相同的位置,这样它们就不会甚至知道你换了(除非他们试图太快地走到最后)。

除此之外,您可以尝试获得真正的高级功能,并尝试找出一种方法来添加成分的URL,而无需一次创建整个URL。这会使它快得多,但可能需要一些复杂的东西来纠正(不太熟悉音频格式)。

答案 1 :(得分:1)

在@samanime回答中,我相信这条负责从块创建blob的行是需要花费大量时间的行。相反,您可以尝试构建blob。你可以这样做:

<body>
    <button class="record">RECORD</button>
    <button class="stop">STOP</button>
    <div class="clips"></div>
    <script>
    if (navigator.mediaDevices) {
        const record = document.querySelector('.record')
        const stop = document.querySelector('.stop')
        const soundClips = document.querySelector('.clips')

        const constraints = { audio: true };
        let blob = new Blob()

        navigator.mediaDevices.getUserMedia(constraints)
            .then(function (stream) {

                const mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });

                record.onclick = function () {
                    mediaRecorder.start();
                    record.style.background = "red";
                    record.style.color = "black";
                }

                stop.onclick = function () {
                    mediaRecorder.stop();
                    record.style.background = "";
                    record.style.color = "";
                }

                mediaRecorder.onstop = function (e) {
                    const clipName = prompt('Enter a name for your sound clip');

                    const clipContainer = document.createElement('article');
                    const clipLabel = document.createElement('p');
                    const audio = document.createElement('audio');
                    const deleteButton = document.createElement('button');

                    clipContainer.classList.add('clip');
                    audio.setAttribute('controls', '');
                    audio.setAttribute('preload', 'metadata');
                    deleteButton.innerHTML = "Delete";
                    clipLabel.innerHTML = clipName;

                    clipContainer.appendChild(audio);
                    clipContainer.appendChild(clipLabel);
                    clipContainer.appendChild(deleteButton);
                    soundClips.appendChild(clipContainer);

                    audio.controls = true;

                    const audioURL = URL.createObjectURL(blob);
                    audio.src = audioURL;

                    deleteButton.onclick = function (e) {
                        evtTgt = e.target;
                        evtTgt.parentNode.parentNode.removeChild(evtTgt.parentNode);
                    }
                }

                mediaRecorder.ondataavailable = function (e) {
                    blob = new Blob([blob, e.data]);
                }
            })
            .catch(function (err) {
                console.log('The following error occurred: ' + err);
            })
    }
    </script>
</body>

不幸的是,blob是不可变的,所以你不能真的&#34;追加&#34;当你走的时候,你必须不断重建一个blob。这可能是也可能不是性能问题。

答案 2 :(得分:1)

为什么寻求不会马上工作

使用Blob网址时,您的音频播放器无法获取有关媒体持续时间的任何信息。我还发现你cannot set it manually。这将阻止您在本机浏览器音频控件的进度条上寻找。所以,不幸的是,我相信你不能使用原生控件。

可能的解决方法是什么?

您可以做的是测量录制会话持续的时间,并将该持续时间传递给播放器控制器。该播放器控制器可以是现有的(例如HowlerJS),也可以是自定义控制器。现有问题是大多数(全部?)不支持手动设置持续时间。如果你深入研究他们的代码,可能会有另一种解决方法,但就目前而言,我认为创建一个自定义播放器会很有趣。

自定义播放器

我创建了一个SoundClip函数,用于创建工作播放器的DOM元素,并允许您设置音频的URL及其持续时间(以秒为单位)。以下是如何使用它:

// Declare a new SoundClip instance
const audio = new SoundClip();

// Get its DOM player element and append it to some container
someContainer.appendChild(audio.getElement());

// Set the audio Url and duration
audio.setSource(audioURL, duration);

如何调整代码以使用它

首先,您必须测量录制时间:

// At the top of your code, create an Object that will hold that data
const recordingTimes = {};

record.onclick = function() {
    // Record the start time
    recordingTimes.start = +new Date();
    /* ... */
}

stop.onclick = function() {
    // Record the end time
    recordingTimes.end = +new Date();
    // Calculate the duration in seconds
    recordingTimes.duration = (recordingTimes.end - recordingTimes.start) / 1000;
    /* ... */
}

然后,使用audio实例,而不是使用SoundClip DOM元素:

  mediaRecorder.onstop = function(e) {
    /* ... */
    const deleteButton = document.createElement('button');
    // Declare a new SoundClip instance
    const audio = new SoundClip();

    /* ... */

    // Append the SoundClip element to the DOM
    clipContainer.appendChild(audio.getElement());
    clipContainer.appendChild(clipLabel);

    /* ... */
    const audioURL = URL.createObjectURL(blob);

    // Set the audio Url and duration
    audio.setSource(audioURL, recordingTimes.duration);

    /* ... */
  }

然后呢?

然后,你应该能够做你想做的事。我在下面提供了SoundClip函数和CSS的完整代码,但它非常基本且不太时尚。您可以决定自定义它以满足您的需求,或者与市场上的现有播放器一起使用,请记住,您必须破解它才能使其正常工作。

现场演示

https://shrt.tf/so_49886426/

完整代码

这在StackOverflow上不起作用,因为它不允许使用麦克风,但这里是完整的代码:

function SoundClip() {
    const self = {
        dom: {},
        player: {},
        class: 'sound-clip',

        ////////////////////////////////
        // SoundClip basic functions
        ////////////////////////////////

        // ======================
        // Setup the DOM of the player and the player instance
        // [Automatically called on instantiation]
        // ======================
        init: function() {
            //  == Create the DOM elements ==
            // Wrapper
            self.dom.wrapper = self.createElement('div', {
                className: `${self.class} ${self.class}-disabled`
            });
            // Play button
            self.dom.playBtn = self.createElement('div', {
                className: `${self.class}-play-btn`,
                onclick: self.toggle
            }, self.dom.wrapper);
            // Range slider
            self.dom.progress = self.createElement('input', {
                className: `${self.class}-progress`,
                min: 0,
                max: 100,
                value: 0,
                type: 'range',
                onchange: self.onChange
            }, self.dom.wrapper);
            // Time and duration
            self.dom.time = self.createElement('div', {
                className: `${self.class}-time`,
                innerHTML: '00:00 / 00:00'
            }, self.dom.wrapper);

            self.player.disabled = true;
            //  == Create the Audio player ==
            self.player.instance = new Audio();
            self.player.instance.ontimeupdate = self.onTimeUpdate;
            self.player.instance.onended = self.stop;

            return self;
        },
        // ======================
        // Sets the URL and duration of the audio clip
        // ======================
        setSource: function(url, duration) {
            self.player.url = url;
            self.player.duration = duration;
            self.player.instance.src = self.player.url;
            // Enable the interface
            self.player.disabled = false;
            self.dom.wrapper.classList.remove(`${self.class}-disabled`);
            // Update the duration
            self.onTimeUpdate();
        },
        // ======================
        // Returns the wrapper DOM element
        // ======================
        getElement: function() {
            return self.dom.wrapper;
        },

        ////////////////////////////////
        // Player functions
        ////////////////////////////////

        // ======================
        // Plays or pauses the player
        // ======================
        toggle: function() {
            if (!self.player.disabled) {
                self[self.player.playing ? 'pause' : 'play']();
            }
        },
        // ======================
        // Starts the player
        // ======================
        play: function() {
            if (!self.player.disabled) {
                self.player.playing = true;
                self.dom.playBtn.classList.add(`${self.class}-playing`);
                self.player.instance.play();
            }
        },
        // ======================
        // Pauses the player
        // ======================
        pause: function() {
            if (!self.player.disabled) {
                self.player.playing = false;
                self.dom.playBtn.classList.remove(`${self.class}-playing`);
                self.player.instance.pause();
            }
        },
        // ======================
        // Pauses the player and resets its currentTime
        // ======================
        stop: function() {
            if (!self.player.disabled) {
                self.pause();
                self.seekTo(0);
            }
        },
        // ======================
        // Sets the player's current time
        // ======================
        seekTo: function(sec) {
            if (!self.player.disabled) {
                self.player.instance.currentTime = sec;
            }
        },

        ////////////////////////////////
        // Event handlers
        ////////////////////////////////

        // ======================
        // Called every time the player instance's time gets updated
        // ======================
        onTimeUpdate: function() {
            self.player.currentTime = self.player.instance.currentTime;
            self.dom.progress.value = Math.floor(
                self.player.currentTime / self.player.duration * 100
            );
            self.dom.time.innerHTML = `
                ${self.formatTime(self.player.currentTime)}
                /
                ${self.formatTime(self.player.duration)}
            `;
        },
        // ======================
        // Called every time the user changes the progress bar value
        // ======================
        onChange: function() {
            const sec = self.dom.progress.value / 100 * self.player.duration;
            self.seekTo(sec);
        },

        ////////////////////////////////
        // Utility functions
        ////////////////////////////////

        // ======================
        // Create DOM elements,
        // assign them attributes and append them to a parent
        // ======================
        createElement: function(type, attributes, parent) {
            const el = document.createElement(type);
            if (attributes) {
                Object.assign(el, attributes);
            }
            if (parent) {
                parent.appendChild(el);
            }
            return el;
        },
        // ======================
        // Formats seconds into [hours], minutes and seconds
        // ======================
        formatTime: function(sec) {
            const secInt = parseInt(sec, 10);
            const hours = Math.floor(secInt / 3600);
            const minutes = Math.floor((secInt - (hours * 3600)) / 60);
            const seconds = secInt - (hours * 3600) - (minutes * 60);

            return (hours ? (`0${hours}:`).slice(-3) : '') +
                   (`0${minutes}:`).slice(-3) +
                   (`0${seconds}`).slice(-2);
        }
    };

    return self.init();
}

if (navigator.mediaDevices) {
  const record = document.querySelector('.record');
  const stop = document.querySelector('.stop');
  const soundClips = document.querySelector('.clips');
  // Will hold the start time, end time and duration of recording
  const recordingTimes = {};

  const constraints = {
    audio: true
  };
  let chunks = [];

  navigator.mediaDevices.getUserMedia(constraints)
    .then(function(stream) {

      const mediaRecorder = new MediaRecorder(stream, {
        mimeType: 'audio/webm'
      });

      record.onclick = function() {
        // Record the start time
        recordingTimes.start = +new Date();
        mediaRecorder.start();
        record.style.background = "red";
        record.style.color = "black";
      }

      stop.onclick = function() {
        // Record the end time
        recordingTimes.end = +new Date();
        // Calculate the duration in seconds
        recordingTimes.duration = (recordingTimes.end - recordingTimes.start) / 1000;
        mediaRecorder.stop();
        record.style.background = "";
        record.style.color = "";
      }

      mediaRecorder.onstop = function(e) {
        const clipName = prompt('Enter a name for your sound clip');

        const clipContainer = document.createElement('article');
        const clipLabel = document.createElement('p');
        const deleteButton = document.createElement('button');
        // Declare a new SoundClip
        const audio = new SoundClip();

        clipContainer.classList.add('clip');
        deleteButton.innerHTML = "Delete";
        clipLabel.innerHTML = clipName;

        // Append the SoundClip element to the DOM
        clipContainer.appendChild(audio.getElement());
        clipContainer.appendChild(clipLabel);
        clipContainer.appendChild(deleteButton);
        soundClips.appendChild(clipContainer);

        const blob = new Blob(chunks);
        chunks = [];
        const audioURL = URL.createObjectURL(blob);

        // Set the audio Url and duration
        audio.setSource(audioURL, recordingTimes.duration);

        deleteButton.onclick = function(e) {
          evtTgt = e.target;
          evtTgt.parentNode.parentNode.removeChild(evtTgt.parentNode);
        }
      }

      mediaRecorder.ondataavailable = function(e) {
        chunks.push(e.data);
      }
    })
    .catch(function(err) {
      console.log('The following error occurred: ' + err);
    })
}
.sound-clip, .sound-clip * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
.sound-clip {
    border: 1px solid #9ee0ff;
    padding: .5em;
    font-family: Arial, Helvetica, sans-serif;
}
.sound-clip.sound-clip-disabled {
    opacity: .5;
}
.sound-clip-play-btn {
    display: inline-block;
    text-align: center;
    width: 2em;
    height: 2em;
    border: 1px solid #12b2ff;
    color: #12b2ff;
    cursor: pointer;
    vertical-align: middle;
    margin-right: .5em;
    transition: all .2s ease;
}
.sound-clip-play-btn:before {
    content: "►";
    line-height: 2em;
}
.sound-clip-play-btn.sound-clip-playing:before {
    content: "❚❚";
    line-height: 2em;
}
.sound-clip-play-btn:not(.sound-clip-disabled):hover {
    background: #12b2ff;
    color: #fff;
}
.sound-clip-progress {
    line-height: 2em;
    vertical-align: middle;
    width: calc(100% - 3em);
}
.sound-clip-time {
    text-align: right;
}
<button class="record">RECORD</button>
<button class="stop">STOP</button>
<div class="clips"></div>