【问题标题】:JavaScript - how to immediately rewind through recorded audioJavaScript - 如何立即倒带录制的音频
【发布时间】:2018-04-17 19:54:32
【问题描述】:

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

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>

【问题讨论】:

    标签: javascript html5-audio web-mediarecorder


    【解决方案1】:

    为什么搜索不能马上起作用

    使用 Blob URL 时,您的音频播放器不会获得有关媒体持续时间的任何信息。我还发现你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 DOM 元素,而是使用 SoundClip 实例:

      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>

    【讨论】:

      【解决方案2】:

      详细说明@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 是不可变的,所以你不能真正“附加”到它,你必须不断地重新创建一个 blob。这可能是也可能不是性能问题。

      【讨论】:

      • 这不是(仅) 性能问题。我做了一些测试,问题是 Blob 不包含有关声音持续时间的信息,直到你真正播放它直到结束。我正在研究一个解决方案(在变量中节省时间),但最终的解决方案可能是你和我正在研究的解决方案之间的混合
      • 我尝试过做类似的事情,正如@blex 提到的,Blob 不包含有关声音持续时间的信息。我认为节省录制时间是个好主意。我不确定是否可以明确地为音频元素提供持续时间
      【解决方案3】:

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

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

      除此之外,您还可以尝试变得更高级,并尝试找出一种方法来分段构建 URL,而无需一次性创建整个 URL。这会使其更快,但可能需要一些复杂的东西才能正确(不是非常熟悉音频格式)。

      【讨论】:

      • 通过 URL 我想你真的是指 Blob?可能值得澄清。
      • 有点,我指的是从 Blob 创建的数据 URL。基本上,我总是在每次接收数据时构建 URL,以便最近一个可用。如果它可以在Blob 级别有效地完成,那就更好了。不确定两者中哪一个是最耗时的。
      • 非常感谢大家的回复!!
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-08-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-06-15
      相关资源
      最近更新 更多