Build a WebRTC Webcam Video Recorder Using MediaRecorder.js Library Web App Using HTML5 & Javascript Full Project For Beginners

 

 

 

index.html

 

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Media Stream Recorder by Quickblox</title>

    <meta name="viewport" content="width=device-width, initial-scale=1">

    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <link rel="icon" href="https://docsdev.quickblox.com/themes/quickblox/templates/main_resources/favicon.ico">

    <link rel="stylesheet" href="css/spectre.min.css">
    <style>
html {
    overflow-y: scroll;
    height: 100%;
}

body {
    min-height: 100%;
    display: flex;
    flex-direction: column;
}

.content {
    flex: 1;
}

.wrap {
    width: 80%;
    max-width: 1120px;
    margin: 0 auto;
}

.title_main {
    font-weight: 700;
}

.title_sub {
    font-size: .6em;
    font-weight: 300;
}

.source {
    width: 100%;

    position: absolute;
    top: 5px;
    left: 5px;
}

.video {
    background-color: #efefef;
}

.video-responsive .video {
    height: 100%;
    left: 0;
    position: absolute;
    top: 0;
    width: 100%;
}

.footer {
    padding: 1em 0;

    background: #efefef;
    text-align: center;
}

.footer_desc {
    width: 80%;
    margin: 0 auto;

}

.notify {
    opacity: 0;

    max-width: 480px;
    position: fixed;
    bottom: 10%;
    right: 5%;

    transition: opacity .4s ease;
}

.notify-active {
    opacity: 1;
}

/* Changes for spectre */
.form-select {
    max-width: 100%;
}

.card {
    position: relative;
}

.form-select:not([multiple]) {
    background-color: rgba(255, 255, 255, .4);
}
</style>
</head>
<body>
    <div class="content">
        <header class="title wrap">
            <h1 class="title_main text-center">Media Stream Recorder
                <span class="title_sub">by Quickblox</span>
            </h1>
        </header>

        <aside class="toast wrap">
            This demo requires Firefox 49, Chrome 49, Chrome for Android 53, Opera 41 or later. The https is required.
        </aside>

        <main>
            <div class="columns wrap">
                <div class="column col-6 col-xs-12">
                    <div class="card j-card">
                        <div class="card-image">
                            <div class="video-responsive">
                                <video class=" j-video_local video" controls muted></video>
                            </div>
                        </div>
                        
                        <div class="source">
                            <div class="form-group">
                                <select id="j-audioSource" class="invisible form-select select-sm">
                                </select>
                            </div>

                            <div class="form-group">
                                <select id="j-videoSource" class="invisible form-select select-sm">
                                </select>
                            </div>

                            <div class="form-group">
                                <select id="j-mimeTypes" class="invisible form-select select-sm">
                                </select>
                            </div>
                        </div>

                        <div class="card-footer">
                            <h5 class="card-title">Recording</h5>
                            <div class="btn-group btn-group-block">
                                <button class="btn j-start">Start</button>
                                <button class="btn j-pause" disabled>Pause</button>
                                <button class="btn j-resume" disabled>Resume</button>
                                <button class="btn j-stop" disabled>Stop</button>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="column col-6 col-xs-12">
                    <div class="card j-result-card">
                        <div class="card-image">
                            <div class="video-responsive">
                                <video class="j-video_result video" controls></video>
                            </div>
                        </div>

                        <div class="card-footer">
                            <h5 class="card-title">Record</h5>
                            <div class="btn-group btn-group-block">
                                <button class="btn j-clear" disabled>Clear</button>
                                <button class="btn j-download" disabled>Download</button>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </main>
    </div>

    <footer class="footer">
        <p class="lead ">
            See <a href="../docs/index.html" target="_blank">docs</a> and <a href="https://github.com/QuickBlox/javascript-media-recorder/" target="_blank">source code</a>
        </p>
    </footer>

    <div class="j-notify notify toast toast-danger"></div>

    <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
    <script src="../qbMediaRecorder.js"></script>
    <script>
/* JSHint inline rules */
/* jshint node: true, browser: true */
/* globals QBMediaRecorder, Promise */

'use strict';

var rec;

var notify = {
    ui: document.querySelector('.j-notify'),
    hide: function() {
        this.ui.classList.remove('notify-active');
    },
    show: function(txt) {
        var n = this;

        n.ui.textContent = txt;
        n.ui.classList.add('notify-active');

        var timerId = setTimeout(function() {
            n.hide();
        }, 5000);
    }
};

var resultCard = {
    blob: null, // saved a blob after stopped a record
    ui: {
        wrap: document.querySelector('.j-result-card'),
        video: document.querySelector('.j-video_result'),
        clear: document.querySelector('.j-clear'),
        download: document.querySelector('.j-download')
    },
    toggleBtn: function(state) {
        this.ui.clear.disabled = state;
        this.ui.download.disabled = state;
    },
    attachVideo: function(blob) {
        this.ui.video.src = URL.createObjectURL(blob);

        this.ui.clear.disabled = false;
        this.ui.download.disabled = false;
    },
    detachVideo: function() {
        this.blob = null;
        this.ui.video.src = '';

        this.ui.clear.disabled = true;
        this.ui.download.disabled = true;
    },
    setupListeners: function(rec) {
        var self = this;

        var evClear = new CustomEvent('clear');
        var evDownload = new CustomEvent('download');

        self.ui.clear.addEventListener('click', function() {
            self.ui.video.pause();
            self.detachVideo();
            
            self.ui.wrap.dispatchEvent(evClear);
        });
        
        self.ui.download.addEventListener('click', function() {
            self.ui.wrap.dispatchEvent(evDownload);
        });
    }
};

var inputCard = {
    audioRecorderWorkerPath: '../qbAudioRecorderWorker.js',
    stream: null,
    devices: {
        audio: [],
        video: []
    },
    ui: {
        wrap: document.querySelector('.j-card'),
        video: document.querySelector('.j-video_local'),

        start: document.querySelector('.j-start'),
        stop: document.querySelector('.j-stop'),
        pause: document.querySelector('.j-pause'),
        resume: document.querySelector('.j-resume'),

        selectAudioSource: document.getElementById('j-audioSource'),
        selectVideoSource: document.getElementById('j-videoSource'),
        selectMimeTypeFormats: document.getElementById('j-mimeTypes')
    },
    _createOptions: function(type) {
        var docfrag = document.createDocumentFragment();

        /* create a default option */
        var optDef = document.createElement('option');
            optDef.textContent = `Choose an input ${type}-device`;
            optDef.value = 'default';

        docfrag.appendChild(optDef);

        /* create a options with available sources */
        this.devices[type].forEach(function(device, index) {
            var option = document.createElement('option');

            option.value = device.deviceId;
            option.textContent = device.label || `${index + 1} ${type} source`;

            docfrag.appendChild(option);
        });

        /* create a option which off a type a media */
        var optOff = document.createElement('option');
        optOff.textContent = `Off ${type} source`;
        optOff.value = 0;

        docfrag.appendChild(optOff);

        return docfrag;
    },
    _createMimeTypesOptions: function(mimeTypes) {
        var docfrag = document.createDocumentFragment();

        mimeTypes.forEach(function(mimeType) {
            var option = document.createElement('option');

            option.value = mimeType;
            option.textContent = mimeType;

            if (mimeType.includes('video')) {
                option.classList.add('j-videoMimeType');
            } else {
                option.classList.add('j-audioMimeType');
                option.disabled = true;
            }

            docfrag.appendChild(option);
        });

        return docfrag;
    },
    _processDevices: function(devices) {
        var self = this;

        var docfragAudio = document.createDocumentFragment(),
            docfragVideo = document.createDocumentFragment();

        devices.forEach(function(device) {
            if(device.kind.indexOf('input') !== -1) {
                if(device.kind === 'audioinput') {
                    /* set audio source to collection */
                    self.devices.audio.push(device);
                }

                if(device.kind === 'videoinput') {
                    /* set video source to collection */
                    self.devices.video.push(device);
                }
            }
        });

        if(self.devices.audio.length > 0) {
            self.ui.selectAudioSource.appendChild( self._createOptions('audio') );
            self.ui.selectAudioSource.classList.remove('invisible');
        }

        if(self.devices.video.length > 0) {
            self.ui.selectVideoSource.appendChild( self._createOptions('video') );
            self.ui.selectVideoSource.classList.remove('invisible');
        }

        if(QBMediaRecorder.getSupportedMimeTypes().length) {
            var audioMimeTypes = QBMediaRecorder.getSupportedMimeTypes("audio"),
                videoMimeTypes = QBMediaRecorder.getSupportedMimeTypes("video"),
                allMimeTypes = videoMimeTypes.concat(audioMimeTypes);

            self.ui.selectMimeTypeFormats.appendChild( self._createMimeTypesOptions(allMimeTypes) );
            self.ui.selectMimeTypeFormats.classList.remove('invisible');
        }
    },
    getDevices: function() {
        var self = this;

        navigator.mediaDevices.enumerateDevices()
            .then(function(devices) {
                 self._processDevices(devices);
            })
    },
    attachStreamToSource: function() {
        this.ui.video.pause();

        try {
          this.ui.video.srcObject = null;
          this.ui.video.srcObject = this.stream;
        } catch (error) {
          this.ui.video.src = '';
          this.ui.video.src = URL.createObjectURL(this.stream);
        }

        this.ui.video.play();
    },
    getUserMedia: function(attrs) {
        var constraints = attrs || { audio: true, video: true };

        return navigator.mediaDevices.getUserMedia(constraints)
    },
    _getSources: function() {
        var sVideo = this.ui.selectVideoSource,
            sAudio = this.ui.selectAudioSource,
            selectedAudioSource = sAudio.options[sAudio.selectedIndex].value,
            selectedVideoSource = sVideo.options[sVideo.selectedIndex].value;

        var constraints = {};

        if(selectedAudioSource === 'default') {
            constraints.audio = true;
        } else if(selectedAudioSource === '0') {
            constraints.audio = false;
            this._toggleAudioTypesSelect(true);
        } else {
            constraints.audio = {deviceId: selectedAudioSource};
        }

        if(selectedVideoSource === 'default') {
            constraints.video = true;
        } else if(selectedVideoSource === '0') {
            constraints.video = false;
        } else {
            constraints.video = {deviceId: selectedVideoSource};
        }

        this._toggleAudioTypesSelect(constraints.video);
        this._toggleVideoTypesSelect(!constraints.video);

        return constraints;
    },
    _toggleAudioTypesSelect: function(state) {
        var audioTypes = document.getElementsByClassName('j-audioMimeType');

        for (var i = 0; i < audioTypes.length; i++) {
            audioTypes[i].disabled = state;
        }
    },
    _toggleVideoTypesSelect: function(state) {
        var videoTypes = document.getElementsByClassName('j-videoMimeType');

        for (var i = 0; i < videoTypes.length; i++) {
            videoTypes[i].disabled = state;
        }
    },
    _stopStreaming: function() {
        this.stream.getTracks().forEach(function(track) {
            track.stop();
        });
    },
    _setupListeners: function() {
        var self = this;

        var evStart = new CustomEvent('started');
        var evPause = new CustomEvent('paused');
        var evResume = new CustomEvent('resumed');
        var evStop = new CustomEvent('stopped');
        var evChange = new CustomEvent('changed');

        self.ui.start.addEventListener('click', function() {
            self.ui.start.disabled = true;
            self.ui.resume.disabled = true;

            self.ui.stop.disabled = false;
            self.ui.pause.disabled = false;

            self.ui.selectMimeTypeFormats.disabled = true;

            self.ui.wrap.dispatchEvent(evStart);
        });

        self.ui.stop.addEventListener('click', function() {
            self.ui.start.disabled = false;

            self.ui.stop.disabled = true;
            self.ui.pause.disabled = true;
            self.ui.resume.disabled = true;

            self.ui.selectMimeTypeFormats.disabled = false;

            self.ui.wrap.dispatchEvent(evStop);
        });

        self.ui.pause.addEventListener('click', function() {
            self.ui.start.disabled = true;
            self.ui.pause.disabled = true;

            self.ui.resume.disabled = false;
            self.ui.stop.disabled = false;

            self.ui.wrap.dispatchEvent(evPause);
        });

        self.ui.resume.addEventListener('click', function() {
            self.ui.start.disabled = true;
            self.ui.resume.disabled = true;

            self.ui.pause.disabled = false;
            self.ui.stop.disabled = false;

            self.ui.wrap.dispatchEvent(evResume);
        });

        function handleSources() {
            var constrains = self._getSources();
            
            self._stopStreaming();
            self.stream = null;

            self.getUserMedia(constrains).then(function(stream) {
                self.stream = stream;
                self.attachStreamToSource();

                self.ui.wrap.dispatchEvent(evChange);
            });
        }

        function handleRecordMimeType() {
            var sMimeType = self.ui.selectMimeTypeFormats,
                selectedMimeType = sMimeType.options[sMimeType.selectedIndex].value;

            rec.toggleMimeType(selectedMimeType);
        }

        self.ui.selectAudioSource.addEventListener('change', handleSources);
        self.ui.selectVideoSource.addEventListener('change', handleSources);
        self.ui.selectMimeTypeFormats.addEventListener('change', handleRecordMimeType);
    },
    init: function() {
        var self = this;

        return new Promise(function(resolve, reject) {
            self.getUserMedia()
                .then(function(stream) {
                    self.stream = stream;
                    self.attachStreamToSource();
                    self.getDevices();
                    self._setupListeners();

                    resolve();
                }).catch(function(error) {
                    reject(error);
                })
        });
    }
};

/* Start !FUN */
inputCard.init()
    .then(function() {
        initRecorder();
    })
    .catch(function(error) {
        notify.show(`Error: ${error.name}`);
    });

function initRecorder() {
    var opts = {
        onstop: function onStoppedRecording(blob) {
            resultCard.blob = blob;
            resultCard.attachVideo(blob);
        },
        workerPath: inputCard.audioRecorderWorkerPath
    };

    rec = new QBMediaRecorder(opts);

    resultCard.setupListeners();

    inputCard.ui.wrap.addEventListener('started', function() {
        rec.start(inputCard.stream);
    }, false);

    inputCard.ui.wrap.addEventListener('paused', function() {
        rec.pause();
    }, false);

    inputCard.ui.wrap.addEventListener('resumed', function() {
        rec.resume();
    }, false);

    inputCard.ui.wrap.addEventListener('changed', function() {
        if (rec.getState() === 'recording') {
            rec.change(inputCard.stream);
        }
    }, false);

    inputCard.ui.wrap.addEventListener('stopped', function() {
        rec.stop();
        resultCard.toggleBtn(false);
    }, false);

    resultCard.ui.wrap.addEventListener('download', function() {
        rec.download(null, resultCard.blob);
    }, false);
}
</script>
</body>
</html>

 

See also  Build a Wikipedia API Search Bar or Engine in Browser Using Javascript Full Project For Beginners

 

DOWNLOAD FULL SOURCE CODE

 

 

Leave a Reply