背景: 我正在开发一个 Django 项目,使用 webRTC 进行视频/音频录制以进行用户注册。 我使用这个存储库(演示)作为我的代码的基础,然后添加了我的功能。
这是我所有更改的工作流程:
问题: 使用
Firefox 70.0.1 (64 bits)
:在第 3 步中视频/音频同步完美。
使用 Chrome 84.0.4147.105 (64 bits)
:在步骤 3 中音频同步延迟约 3 秒。
要求: 我做错什么了吗? 请问有什么窍门可以解决吗?
我的代码如下:
HTML 代码:
<div class="video-div">
<h2>Etape 1</h2>
<div class="video-content">
<video id="gum" class="video-show" poster="{% static 'core/images/video-show-poster.png' %}" playsinline autoplay muted></video>
<video id="recorded" class="video-hide" playsinline></video>
<p class="timer"></p>
</div>
<button class="btn start-recording" id="record">Enregistrer ma vidéo</button>
</div>
<div class="echo-cancellation">
<h4>Media Stream Constraints options</h4>
<p>Echo cancellation: <input type="checkbox" id="echoCancellation"></p>
</div>
<div>
<span id="errorMsg"></span>
</div>
<form id="registration-form" action="" method="POST" enctype="multipart/form-data" novalidate>
{% csrf_token %}
{% for field in form_registration %}
{% if field.name != "registration_video" %}
<div class="form-group">
<div> {{ field.label_tag }} {{ field }}</div>
</div>
{% endif %}
{% endfor %}
{% for field in form_location %}
<div class="form-group">
<div> {{ field.label_tag }} {{ field }}</div>
</div>
{% endfor %}
<input type="submit" value="Inscription" class="register-submit-btn btn">
</form>
JS代码:
<!-- Script to register user media. Adaptated to current app thanks to this resource: https://github.com/webrtc/samples/tree/gh-pages/src/content/getusermedia/record -->
<script>
let mediaRecorder;
let recordedBlobs;
const errorMsgElement = document.querySelector('span#errorMsg');
const recordedVideo = document.querySelector('video#recorded');
const recordButton = document.querySelector('button#record');
recordButton.addEventListener('click', async () => {
const hasEchoCancellation = document.querySelector('#echoCancellation').checked;
const constraints = {
audio: {
echoCancellation: {exact: hasEchoCancellation}
},
video: {
width: 290, height: 240
}
};
console.log('Using media constraints:', constraints);
await init(constraints);
//Hiding the recorded video if user record a new one, show the gum video (livecam) instead.
if(document.getElementById('gum').hasAttribute('class', 'video-hide')){
document.getElementById('gum').classList.remove('video-hide');
document.getElementById('gum').classList.add('video-show');
document.getElementById('recorded').classList.remove('video-show');
document.getElementById('recorded').classList.add('video-hide');
}
startRecording();
});
//Make all necessary action for the video recording
function startRecording()
{
recordedBlobs = [];
let options = {mimeType: 'video/webm;codecs=vp9,opus'};
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
//console.error(`${options.mimeType} is not supported`);
options = {mimeType: 'video/webm;codecs=vp8,opus'};
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
//console.error(`${options.mimeType} is not supported`);
options = {mimeType: 'video/webm'};
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
//console.error(`${options.mimeType} is not supported`);
options = {mimeType: ''};
}
}
}
try {
mediaRecorder = new MediaRecorder(window.stream, options);
} catch (e) {
console.error('Exception while creating MediaRecorder:', e);
errorMsgElement.innerHTML = `Exception while creating MediaRecorder: ${JSON.stringify(e)}`;
return;
}
console.log('Created MediaRecorder', mediaRecorder, 'with options', options);
recordButton.disabled = true;
// When the recording is done we're allowing the user to register a new video
// We're using the recordedBlob as a source to play the recordedVideo automatically
mediaRecorder.onstop = (event) => {
console.log('Recorder stopped: ', event);
console.log('Recorded Blobs: ', recordedBlobs);
document.getElementById('record').textContent = 'Nouvelle tentative';
const superBuffer = new Blob(recordedBlobs, {type: 'video/webm'});
recordedVideo.src = null;
recordedVideo.srcObject = null;
recordedVideo.src = window.URL.createObjectURL(superBuffer);
recordedVideo.controls = true;
};
mediaRecorder.ondataavailable = handleDataAvailable;
mediaRecorder.start();
console.log('MediaRecorder started', mediaRecorder);
//The video should last less that 10 seconds.
//We put a timeout so the video will last less than 10sec. And an interval to display a countdown 5 secondes before the end.
let timerId = setInterval(countDown, 1000);
let i = 10;
function countDown()
{
if(i <= 5 && i > 0 ) {
document.querySelector('.timer').style.display = 'block';
document.querySelector('.timer').textContent = i + ' secondes';
} else if (i < 1){
clearInterval(timerId);
document.querySelector('.timer').textContent = '';
document.querySelector('.timer').style.display = 'none';
}
i--;
}
//When the 10 seconds are done, we're showing the user the recorded video.
setTimeout(function()
{
stopRecording();
recordButton.disabled = false;
document.getElementById('gum').classList.remove('video-show');
document.getElementById('gum').classList.add('video-hide');
document.getElementById('recorded').classList.remove('video-hide');
document.getElementById('recorded').classList.add('video-show');
stream.getTracks().forEach(
function(track)
{
track.stop();
}
);
}, 10000);
}
// If there is data, we push it to the recordedBlob array
function handleDataAvailable(event)
{
console.log('handleDataAvailable', event);
if (event.data && event.data.size > 0) {
recordedBlobs.push(event.data);
}
}
// We're listening to the click on submit button in order add the recordedBlob to the registration_video input thanks to the FormData class.
// Then we're sending the request after creating it with the XMLHttpRequest class.
const form = document.querySelector('form');
form.addEventListener('submit', (evt) => {
let myForm = document.querySelector('form');
let formData = new FormData(myForm);
if(recordedBlobs){
formData.append('registration_video', recordedBlobs[0], encodeURIComponent("nomaleatoire.webm"));
} else {
formData.append('registration_video', '');
}
//Handling form errors
evt.preventDefault();
const request = new XMLHttpRequest();
request.open("POST", "/register", true);
request.onload = function() {
response = JSON.parse(this.response);
if ( response.success == true) {
evt.target.querySelectorAll('input[type="text"], input[type="date"], input[type="number"], input[type="email"], textarea').forEach(
function(inputElem) {
inputElem.value = '';
});
if(document.querySelectorAll('.error-div')){
document.querySelectorAll('.error-div').forEach(
function(errorDiv)
{
errorDiv.remove();
}
)
}
window.location.href = "/";
} else {
errors = Object.assign(JSON.parse(response.errors_location), JSON.parse(response.errors_registration));
if(document.querySelectorAll('.error-div'))
{
document.querySelectorAll('.error-div').forEach(
function(errorDiv)
{
errorDiv.remove();
}
)
}
for (const [key, value] of Object.entries(errors)) {
if(key != 'registration_video') {
div = document.createElement("div")
div.setAttribute('id', 'error-div-' + key);
div.classList.add("error-div")
errorMessage = "";
value.forEach(errorfield => {
errorMessage = errorMessage + " " + errorfield.message
});
div.textContent = errorMessage
inputElem = document.querySelector('#id_' + key)
theform = inputElem.parentElement.parentElement.parentElement;
theform.insertBefore(div,inputElem.parentElement.parentElement)
} else {
//Il faut afficher une erreur pour la vidéo manquante!
errorVideoMessage = document.createElement('div');
errorVideoMessage.textContent = "L'enregistrement de la vidéo est obligatoire.";
errorVideoMessage.classList.add('error-div', 'mt-2');
document.querySelector('.start-recording').before(errorVideoMessage);
}
}
}
};
request.onerror = function() {
// request failed
};
request.send(formData);
});
function stopRecording()
{
mediaRecorder.stop();
}
function handleSuccess(stream)
{
recordButton.disabled = false;
console.log('getUserMedia() got stream:', stream);
window.stream = stream;
const gumVideo = document.querySelector('video#gum');
gumVideo.srcObject = stream;
}
async function init(constraints)
{
try {
const stream = await navigator.mediaDevices.getUserMedia(constraints);
handleSuccess(stream);
} catch (e) {
console.error('navigator.getUserMedia error:', e);
errorMsgElement.innerHTML = `navigator.getUserMedia error:${e.toString()}`;
}
}
</script>
有趣的是,臭名昭著的徽章和 318k plus 无法在 2024 年回答这个问题。