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>
3 Antworten
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
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>
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).
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.