diff --git a/nodejs/public/javascripts/mediarecorder.js b/nodejs/public/javascripts/mediarecorder.js new file mode 100644 index 0000000..3fdda75 --- /dev/null +++ b/nodejs/public/javascripts/mediarecorder.js @@ -0,0 +1,176 @@ +/// @class MediaRecorder +/** Provides a recorder to record fotos, audio and video from a web + browser client using the JavaScript getUserMedia feature. + + @param constraints The constraints as JSON data: + @code + { + video: true, // true if you want to record video + audio: true // true if you want to record audio + } + @endcode + You can record video or audio only or both together. + if not specified, defaults to @ref defaultconstraints. + + @note Special thanks to the following projects: + - For the basics regarding getUserMedia: + http://www.html5rocks.com/en/tutorials/getusermedia/intro/ + - For a simple example of a media recorder: + https://github.com/samdutton/simpl/blob/gh-pages/mediarecorder + + @note Supported browsers are: + - Firefox 29 or later + - Chrome 47 or later, with @c "Enable experimental Web Platform + features" enabled from @c "chrome://flags" + + @note This class must be used from within a secure context. A + secure context is an encrypted SSL connection through HTTPS, or + the special address @c localhost. + + @todo Why are mediaSource and sourceBuffer needed? They are not + further referenced. +*/ +function MediaStreamRecorder(constraints) { + + /// @name private variables + ///@{ + var events = []; + var mediaSource = new MediaSource(); + var stream; + var mediaRecorder; + var recordedBlobs = []; + var sourceBuffer; + var defaultconstraints = { + audio: true, + video: true + }; + ///@} + + /// @name internal event handlers + ///@{ + + /// Create Source Buffer on Event Source Open + function handleSourceOpen(event) { + sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp8"'); + } + + /// Store Data in local Binary Large Object + function handleDataAvailable(event) { + if (event.data && event.data.size > 0) { + recordedBlobs.push(event.data); + } + } + + /// Propagate @c ready Event to Registered Handler + function handleReady() { + if (events['ready']) events['ready'](); + } + + /// Propagate @c start Event to Registered Handler + function handleStart() { + if (events['stop']) events['start'](); + } + + /// Propagate @c stop Event to Registered Handler + function handleStop(event) { + if (events['stop']) events['stop'](event); + } + + ///@} + + /// @name public methods + ///@{ + + /// Register Eventhandler + /** @param eventname The following events are available: + - @c ready Video preview stream is ready for use + - @c stop Recording has stopped, parameter: @event + - @c start Recording has started + + @param eventhandler callback function to be called when the + event occurs. + */ + this.on = function(eventname, eventhandler) { + events[eventname] = eventhandler; + } + + /// Get Stream to the Preview + /** @return Stream prepared to be used in a HTML @c src attribute + within a @c audio or @c video tag. */ + this.preview = function() { + return window.URL ? window.URL.createObjectURL(stream) : stream; + } + + /// Get Stream to the Recording + /** @return Stream prepared to be used in a HTML @c src attribute + within a @c audio or @c video tag, or to be used in a + HTML @c href attribute in a @c a tag for downloading + the recording. */ + this.recording = function() { + var buff = new Blob(recordedBlobs, {type: 'video/webm'}); + return window.URL ? window.URL.createObjectURL(buff) : buff; + } + + /// Start Stream Recording + /** @throws Exception if browser is not supported */ + this.start = function() { + // The nested try blocks will be simplified when Chrome 47 moves to Stable + var options = {mimeType: 'video/webm'}; + recordedBlobs = []; + try { + mediaRecorder = new MediaRecorder(stream, options); + } catch (e0) { + try { + options = {mimeType: 'video/webm,codecs=vp9'}; + mediaRecorder = new MediaRecorder(stream, options); + } catch (e1) { + try { + options = 'video/vp8'; // Chrome 47 + mediaRecorder = new MediaRecorder(stream, options); + } catch (e2) { + throw { + text: 'MediaRecorder is not supported by browser', + e0: e0, + e1: e1, + e2: e2 + }; + } + } + } + mediaRecorder.onstop = handleStop; + mediaRecorder.ondataavailable = handleDataAvailable; + mediaRecorder.start(10); // collect 10ms of data + handleStart(); + } + + /// Stop Stream Recording + /** Use recording() to get access to the result. */ + this.stop = function () { + if (mediaRecorder) mediaRecorder.stop(); + delete mediaRecorder; mediaRecorder = null; + } + + /// Close Preview Stream + /** Closes the stream and releases the camera. This should always + be called to cleanup, when the camera is no more needed. */ + this.release = function() { + stop(); + stream.getTracks().forEach(function(track) {track.stop()}); + delete stream; stream = null; + } + + ///@} + + /// @name initialization + ///@{ + mediaSource.addEventListener('sourceopen', handleSourceOpen, false); + if (!constraints) constraints = defaultconstraints; + navigator.mediaDevices.getUserMedia(constraints) + .then(function(s) { + stream = s; + handleReady(); + }).catch(function(error) { + throw error; + }); + ///@} +} diff --git a/nodejs/public/javascripts/safechat.js b/nodejs/public/javascripts/safechat.js index 368b555..c96e3a5 100644 --- a/nodejs/public/javascripts/safechat.js +++ b/nodejs/public/javascripts/safechat.js @@ -343,67 +343,72 @@ function guessfilename(mimetype, user, date) { function attachments(files, id, from, date) { if (files) files.forEach(function(file) { if (!file.name) file.name = guessfilename(file.type, from, date); - var img = document.createElement('img'); - img.title = file.name; + var a = document.createElement('a'); + a.href = file.content; + a.download = file.name; + a.target = '_blank'; if (file.type.match('^image/')) { + var img = document.createElement('img'); + img.title = file.name; img.src = file.content; + a.appendChild(img); + } else if (file.type.match('^video/')) { + var video = document.createElement('video'); + video.controls = true; + video.title = file.name; + video.src = file.content; + a.appendChild(video); } else { + var img = document.createElement('img'); + img.title = file.name; img.src = "images/Document_sans_PICOL-PIctorial-COmmunication-Language.svg"; + a.appendChild(img); } - var a = document.createElement('a'); - a.href = file.content; - a.download = file.name; - a.target = '_blank'; - a.appendChild(img); $(id).append(a); }); } -// function getUserMedia() { -// return navigator.mediaDevices.getUserMedia || navigator.getUserMedia -// || navigator.webkitGetUserMedia || navigator.mozGetUserMedia -// || navigator.msGetUserMedia; -// } +var recorder; -var mediarecorder = null; -var mediastream = null; -function record() { - mediarecorder = mediastream.record(); -} function done() { - mediarecorder.getRecordedData(function(videoblob) { - previewfile(videoblob, "video/webm"); - }); - mediarecorder = null; + if (recorder) { + recorder.stop(); + previewfile(recorder.recording(), "video/webm"); + abort(); + } } function abort() { - mediarecorder = null; - mediastream.getTracks().forEach(function(track) {track.stop()}); - mediastream = null; + if (recorder) { + $("#videorecorder").hide(); + recorder.release(); + delete recorder; recorder = null; + } } /// Record Video from builtin camera function recordvideo() { - $("#videorecorder").show(); - if (mediastream) { - mediarecorder = mediastream.record(); - } try { - navigator.mediaDevices.getUserMedia({audio: true, video: true}) - .then(function(stream) { - console.log(stream); - mediastream = stream; - var video = $("#videorecorder video"); - video.attr("src", window.URL.createObjectURL(mediastream)); - }).catch(function() { - error("capture video failed", false); - }); + abort(); + $("#videorecorder").show(); + recorder = new MediaStreamRecorder({ + video: { + mandatory: { + maxWidth: 400, + maxHeight: 400 + } + }, + audio: true + }); + recorder.on("ready", function() { + $("#videorecorder video").attr("src", recorder.preview()); + recorder.start(); + }); + recorder.on('') } catch (e) { console.log(e); error("cannot access camera", true); } - } function previewfile(content, type, name) { @@ -440,6 +445,7 @@ function previewfile(content, type, name) { filecontent.push({name: name, type: type, content: content}); var video = document.createElement("video"); video.setAttribute("controls", "controls"); + video.setAttribute("loop", "loop"); video.setAttribute("src", content); video.setAttribute("title", name); $("#preview").append(video); diff --git a/nodejs/public/stylesheets/safechat.css b/nodejs/public/stylesheets/safechat.css index dbba77f..32061a1 100644 --- a/nodejs/public/stylesheets/safechat.css +++ b/nodejs/public/stylesheets/safechat.css @@ -255,6 +255,21 @@ label[for=send] img { width: 100%; } -#preview img { +#msgs .msg .text video { + border-radius: 2ex; + -moz-border-radius: 2ex; + -webkit-border-radius: 2ex; + display: block; + width: 100%; +} + +#videorecorder { + max-width: 100%; + width: auto; + height: auto; + text-align: center; +} + +#preview img, #preview video { height: 4em; } diff --git a/nodejs/views/index.ejs b/nodejs/views/index.ejs index 332fad2..b33261c 100644 --- a/nodejs/views/index.ejs +++ b/nodejs/views/index.ejs @@ -7,6 +7,7 @@ + @@ -143,13 +144,12 @@ -
+