WebRTC Audio/Video not syncing - audio delay

Viewed 1046

Background: I'm working on a Django project using webRTC for video/audio recording for a user registration. I used this repository (demo) as a base for my code, then added my features.

Here the workflow with all my changes:

  1. The user click on a button to start the stream.
  2. The stream stops after 10 sec. While recording, the user can see a countdown timer when it has 5 seconds left.
  3. When a video has been registered, I change the recorder (#gum) to the recorded one (#recorded) so the user can play it, pause it etc.
  4. If the user is not satisfied, he can click on the .start-recording button that now has a text content saying "Record new video".
  5. If the user is satisfied and has filled up the rest of the form, on the form submit I use the blob in the registration-video input to save the file.

Issue: Using Firefox 70.0.1 (64 bits): on step 3 Video/Audio sync is perfect. Using Chrome 84.0.4147.105 (64 bits) : on step 3 Audio sync is delayed for about 3 seconds.

Requests: Did I do something wrong? Is there a trick to fix that please?

My code below :

HTML Code:

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


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

0 Answers
Related