Ich habe einen sehr einfachen POC erstellt, um eine Audiodatei mit einem Mikrofon aufzunehmen und den aufgezeichneten Blob an das Audio-Tag-Element im Browser anzuhängen. Das Problem ist, dass ich nach Abschluss der Aufnahme nicht vor und zurück zurückspulen kann, bis die Aufnahme vollständig geladen ist. Anscheinend gibt es ein Problem mit der Dauer. Was ich erreichen möchte, ist ungefähr so:

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

Genau dort, nachdem Sie die Aufnahme beendet haben, können Sie sofort bis zum Ende der Aufnahme zurückspulen, selbst wenn diese 30 Minuten lang ist. Es funktioniert wie Magie. Wie kann dies erreicht werden?

Dies ist der Code, den ich geschrieben habe (meistens aus MDN kopiert). Sie können in jede index.html kopieren und einfügen:

<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>
0
feerlay 17 Apr. 2018 im 22:54

3 Antworten

Beste Antwort

Warum das Suchen nicht sofort funktioniert

Wenn Sie eine Blob-URL verwenden, erhält Ihr Audio-Player keine Informationen über die Dauer des Mediums. Ich habe auch festgestellt, dass Sie nicht manuell einstellen können. Dadurch wird verhindert, dass Sie in der Fortschrittsanzeige der Audio-Steuerelemente des nativen Browsers suchen . Daher glaube ich leider, dass Sie keine nativen Steuerelemente verwenden können.

Was wäre eine mögliche Problemumgehung?

Sie können messen, wie lange die Aufnahmesitzung dauert, und diese Dauer an einen Player-Controller übergeben. Dieser Player-Controller kann ein vorhandener (z. B. HowlerJS) oder ein benutzerdefinierter Controller sein. Das Problem mit einem vorhandenen ist, dass die meisten (alle?) das manuelle Festlegen einer Dauer nicht unterstützen. Es könnte eine andere Problemumgehung dafür geben, wenn Sie sich in ihren Code vertiefen, aber vorerst dachte ich, es würde Spaß machen, einen benutzerdefinierten Player zu erstellen.

Ein benutzerdefinierter Spieler

Ich habe eine SoundClip -Funktion erstellt, die die DOM-Elemente eines funktionierenden Players erstellt und es Ihnen ermöglicht, eine URL für das Audio zusammen mit seiner Dauer in Sekunden festzulegen. So können Sie es verwenden:

// 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);

Wie können Sie Ihren Code anpassen, um ihn zu verwenden?

Zuerst müssen Sie die Zeit messen, die für die Aufnahme benötigt wird:

// 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;
    /* ... */
}

Verwenden Sie dann anstelle eines audio DOM-Elements eine SoundClip Instanz:

  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);

    /* ... */
  }

Und dann?

Dann sollten Sie in der Lage sein, das zu tun, was Sie wollen. Ich habe den vollständigen Code für die SoundClip -Funktion und CSS unten bereitgestellt, aber es ist ziemlich einfach und nicht sehr stilvoll. Sie können entweder entscheiden, ob Sie es an Ihre Bedürfnisse anpassen oder mit einem vorhandenen Player auf dem Markt arbeiten möchten. Denken Sie daran, dass Sie es hacken müssen, damit es funktioniert.

Live-Demo

https://shrt.tf/so_49886426/

Vollständiger Code

Dies funktioniert unter StackOverflow nicht, da das Mikrofon nicht verwendet werden kann. Hier ist jedoch der vollständige Code:

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
blex 17 Apr. 2018 im 22:47

Es sieht so aus, als ob der Code, den Sie haben, das dataURL erst erstellt, wenn Sie stop() drücken. Wenn Sie also 30 Minuten lang aufnehmen, kann es eine Weile dauern, bis Sie alles analysiert haben.

Stattdessen können Sie die URL im Laufe der Zeit aufbauen und im Laufe der Zeit eine neue URL neu erstellen. Wenn Sie auf Stopp klicken, ist die URL im Grunde bereits mit der letzten Version fertig, in der die letzten x Sekunden fehlen. Wenn die letzte Version erstellt wird, tauschen Sie sie aus und platzieren sie an derselben Position, damit sie sich nicht anziehen Ich weiß nicht einmal, dass du getauscht hast (es sei denn, sie versuchen zu schnell bis zum Ende zu kommen).

Darüber hinaus könnten Sie versuchen, wirklich fortgeschritten zu werden und einen Weg zu finden, um die URL additiv in Teilen zu erstellen, ohne die gesamte URL auf einmal erstellen zu müssen. Das würde es viel schneller machen, wird aber wahrscheinlich ein gutes Stück komplizierter Dinge erfordern, um korrekt zu werden (nicht sehr vertraut mit den Audioformaten).

1
samanime 17 Apr. 2018 im 20:00

Ich gehe auf die Antwort von @samanime ein und glaube, dass diese Zeile, die für die Erstellung eines Blobs aus Chunks verantwortlich ist, viel Zeit in Anspruch nimmt. Stattdessen können Sie versuchen, Blob zu erstellen, während Sie gehen. So könnten Sie vorgehen:

<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>

Leider ist der Blob unveränderlich, so dass Sie ihn nicht wirklich "anhängen" können, sondern immer wieder einen Blob neu erstellen müssen. Dies kann ein Leistungsproblem sein oder auch nicht.

2
redFur 17 Apr. 2018 im 20:18