Mise à jour : Mai 2014

Diffuser une webcam en JavaScript

Récupérer la vidéo de sa webcam pour l'envoyer sur un site web n'est à première vue pas chose aisée. Il faut contrôler la webcam, récupérer la vidéo, l'envoyer sur le serveur dans un format adapté et être économe en ressources pour conserver une certaine fluidité.

Un plugin assez malin a été réalisé par Robert Eisele. Nous l'avons repris et modifié pour un fonctionnement simple en streaming. Originellement en JQuery, le plugin est réécrit pour fonctionner en pur JavaScript. Le fichier Flash est également différent puisqu'il repose sur de l'AS3 et offre un meilleur dialogue avec le JavaScript ; il permet notamment de changer les dimensions de la vidéo depuis le JavaScript.

La démo ci-dessous permet d'envoyer la vidéo de votre webcam sur le serveur du site (elle est détruite à l'issue). Avec un autre ordinateur (ou le même) vous pouvez récupérer la vidéo retour. Pour cela, il suffit d'entrer le même mot de passe que la webcam émettrice.

ONv2 est une version alternative (voir plus bas).

Ma WebCam :
Mon mot de passe (si camera)
Vidéo Retour :
Mot de passe interlocuteur
 

Seuls trois fichiers sont nécessaires pour faire fonctionner ce streaming :

  • Une page HTML classique contenant le code JavaScript nécessaire ;
  • Un petit fichier SWF pour accéder à la webcam (eh oui, le Flash est nécessaire) ;
  • Un fichier PHP sur le serveur pour traiter l'arrivée du flux ;

Le Flash permet le contrôle de la webcam ce qui n'est pas possible en JS. Le JS va donc passer par le Flash pour donner des instructions et récupérer le flux vidéo. Deux réglages sont possibles au choix :

  • Retour du flux ligne par ligne vers le JavaScript qui l'encode en JPG base64 avant de l'envoyer,
  • Retour en JPG base64 vers le JavaScript qui se contente de l'envoyer tel quel.

Le client qui visualise la vidéo à distance se contente de charger ces images du serveur et de les renouveler régulièrement pour obtenir une impression de vidéo.

Les images sont compressées en JPG qualité moyenne avant envoi vers le serveur afin de ne pas dépasser la bande passante. Pour une image au format de la démo ci-dessus, il faut ~180ko en PNG, ~15ko en JPG qualité 100 et ~4ko en JPG qualité 60. Ici, nous sommes en 60. En général, l'encodage depuis le SWF est plus rapide qu'avec le JavaScript mais la taille semble un peu plus grosse à qualité égale (à confirmer).

Sur le serveur, les images qui arrivent sont directement enregistrées dans un fichier sans passer par la bibliothèque GD. C'est un fonctionnement très léger. Chaque nouvelle image écrase la précédente.

Le JavaScript :

<script type="text/javascript">
var moi='',data,canvas=document.getElementById('canvas'),pos=0,ctx=null,
    image=[],b=1,n=document.getElementById('stream');
var options={ // --- 1 ---
    width:320,
    height:240,
    swffile:"webcam.swf",
    wrapper:"webcam",
    jpgQuality:60,
    jpgEncode:1, // 0 : encodage JS ; 1 : encodage SWF
    refresh:800 // raffraichissement en ms
};
//
function webcam(){
        // --- 3 ---
    var source='<object id="camObj" type="application/x-shockwave-flash" data="'+options.swffile+'" width="'+options.width+'" height="'+options.height+'"><param name="movie" value="'+options.swffile+'" /><param name="FlashVars" value="width='+options.width+'&height='+options.height+'&jpgEncode='+options.jpgEncode+'&jpgQuality='+options.jpgQuality+'&refresh='+Math.floor(options.refresh*.9)+'&wrapper='+options.wrapper+'" /><param name="allowScriptAccess" value="always" /></object>';
    document.getElementById('camera').innerHTML=source;
    if (canvas.toDataURL){ // --- 2 ---
        ctx=canvas.getContext("2d");
        image=ctx.getImageData(0,0,options.width,options.height);
    }
    else if (jpgEncode!=1) alert("Votre navigateur est vraiment trop vieux. Essayez Firefox.");
    var run = 3;
    (_register=function() {
        var cam=document.getElementById('camObj');
        if(cam&&cam.capture!==undefined){
            webcam.capture=function(x){return cam.capture(x);}
            webcam.turnOff=function(){return cam.turnOff();}       
            webcam.onSave=saveData;
            webcam.camOk=function(){/*la camera est prête*/}
        }
        else if(run==0)return;
        else{run--;window.setTimeout(_register, 1000*(4-run));}
    })();
}
function saveData(data){
    if(options.jpgEncode==0) {
        var col=data.split(";");
        var img=image;
        for (i=0;i<options.width;i++){tmp=parseInt(col[i]);img.data[pos+0]=(tmp>>16)&0xff;img.data[pos+1]=(tmp>>8)&0xff;img.data[pos+2]=tmp&0xff;img.data[pos+3]=0xff;pos+=4;}
        if (pos>=4*options.width*options.height){
            ctx.putImageData(img,0,0);
            // --- 4 ---
            if(b==1) upload("id="+moi+"&image="+canvas.toDataURL("image/jpeg",(options.jpgQuality)/100));
            pos=0;
        }
    } else if(b==1) upload("id="+moi+"&image="+data);
}
function upload(s){
    b=0;
    a=new XMLHttpRequest();
    a.open("POST","upload.php",true);
    a.setRequestHeader('Content-Type', "application/x-www-form-urlencoded; charset=UTF-8");
    a.setRequestHeader("Content-length", s.length);
    a.onreadystatechange=function(){if(a.readyState==4)b=1;}
    a.send(s);
}
function stream_cam(){n.src='webcam/web'+document.getElementById('toi').value+'.jpg?'+new Date().getTime();}
function stream_on(){vue=setInterval('stream_cam();',options.refresh);}
// --- 5 ---
window.onload=function(){webcam();};
</script>

Au chargement, (5) la fonction webcam() est appelée. Dans cette fonction (2), deux cas se présentent :

  • Le navigateur accepte "canvas.toDataURL" : C'est le cas de la très grande majorité des navigateurs en service. La fonction de traitement de l'image webcam sera savedata(). Si le navigateur est trop vieux mais que l'encodage est réalisé par le SWF, c'est également bon.
  • Le navigateur est trop vieux (IE8 et moins) pour encoder en JPG. Une parade existe dans le plugin d'origine mais elle produit une image supérieure à 5Mo. C'est valable pour une capture d'écran mais pas pour du streaming. Ce n'est pas repris ici.

En cliquant sur ON (démo), on déclenche la méthode webcam.capture. Le SWF retourne alors régulièrement l'image avec l'information "onSave" ce qui déclenche la fonction savedata().

Cette fonction utilise les paramètres de l'objet options (1). Les dimensions peuvent être changées sans modifier le fichier SWF.

En encodage par le JS, la fonction savedata() (4) extrait l'image ligne par ligne. Lorsque l'image est entière, elle est encodée et envoyée en ajax vers le serveur uniquement si ce dernier a reçu la précédente image. Sans cette contrainte, une connexion lente provoquerait un décalage dans le temps de la vidéo car toutes les images non traitées seraient en file d'attente ajax.

En encodage directement par le flash, l'image est envoyée de la même façon sans extraction ni codage.

Enfin, la fonction stream_cam() recharge régulièrement l'image du serveur et met à jour l'affichage.

Il n'y a aucun fichier externe à charger en plus (JQuery ou autres).

Pour la partie HTML, sans les INPUT des mots de passe, le minimum serait :

<div id="camera"></div>
<a href="javascript:stream_on();webcam.capture();void(0);">Démarrer le streaming</a>
<div id="retour" style="width:320px;height:240px;"><img id="stream" src="" /></div>
<canvas id='canvas' width='320' height='240' style="display:none;"></canvas>

Coté serveur, le fichier upload.php est très simple :

<?php
if ($_POST && $_POST['image']){
    $im = str_replace(" ","+",strip_tags($_POST['image']));
    $im = substr($im, 1+strrpos($im, ','));
    if ($_POST['id']) file_put_contents("webcam/web".strip_tags($_POST['id']).".jpg", base64_decode($im));
    }
?>

L'image est extraite de la variable POST['image'] et directement enregistrée dans un fichier dont le nom comprend le mot de passe POST['id'] de l'utilisateur.

Malgré la demande de renouvellement de l'image avec ?'+new Date().getTime(), il est possible qu'un cache perturbe le fonctionnement (navigateur, serveur, box, FAI...). Une parade consiste à charger l'image en Ajax pour imposer un dialogue direct avec le serveur. Ca ne fonctionne pas toujours.

Modifier la fin du JavaScript pour obtenir ceci :

<script type="text/javascript">
.../...
var img='',
function loadimg(f){
    aa=new XMLHttpRequest(); aa.open("GET","upload.php?id="+f,true);
    aa.onreadystatechange=function(){if(aa.readyState==4){img=aa.responseText;}}
    aa.send();
}
function stream_cam(){
    loadimg(document.getElementById('toi').value);
    document.getElementById('retour').style.backgroundImage='url('+img+')';
}
function stream_on(){vue=setInterval('stream_cam();',options.refresh);}
window.onload=function(){webcam();};
</script>

Modifier également le fichier upload.php :

<?php
if ($_POST && $_POST['image']){
    $im = str_replace(" ","+",strip_tags($_POST['image']));
    $im = substr($im, 1+strrpos($im, ','));
    if ($_POST['id']){
        file_put_contents("webcam/web".strip_tags($_POST['id']).".txt","data:image/jpeg;base64,".$im);
        clearstatcache();
        }
    }
else if ($_GET['id']) echo file_get_contents('webcam/web'.strip_tags($_GET['id']).'.txt');
?>

Le navigateur envoie une requête GET en Ajax avec le mot de passe de la caméra. Le serveur lui renvoi l'image encodée en Base64, format initial. Cette image est affichée en background dans le bloc DIV, directement à partir du code base64 intégré dans le CSS.

Sur la démo, ONv1 correspond à la version image et ONv2 à cette version avec chargement Ajax en base64.

Le fichier SWF peut être modifié pour changer des valeurs ou ajouter des fonctionnalités. Le code source est simple et lisible. En revanche, pour être utilisé, il doit être compilé.

Swfmill semble ne pas fonctionner dans les versions 0.3 et les versions plus anciennes de la série 0.2 ne sont plus compatibles avec les dépendances actuelles sous linux.

La méthode consiste donc à passer par le compilateur officiel FlexSdk. Java est l'autre programme indispensable.

Placer dans un dossier le script d'exécution ci-dessous avec les dépendances AS3 et le fichier webcam.as.

#!/bin/sh
RACINE_FLEX="/home/sophie/program/flex_sdk_4.6"
BINAIRE_JAVA6="/usr/lib/java/bin/java"
MAKE="$BINAIRE_JAVA6 -jar $RACINE_FLEX/lib/mxmlc.jar +flexlib=$RACINE_FLEX/frameworks webcam.as"
echo "Executing:"
echo $MAKE
echo " "
echo `$MAKE`

Il suffit d'exécuter le script pour créer le fichier SWF. Le fichier AS est modifiable aisément, avec quelques bases en AS3 ou un peu de patience. Le voici :

/* Ajax Streaming Webcam */
/* V 1.1 */
/* Webcam library for streaming in JPG to a server */
/* Copyright (c) 2014 Jacques Malgrange */
/* http://www.boiteasite.fr */
/* Licensed under the MIT License */
/* http://opensource.org/licenses/MIT */
 
package {
// Bibliothèques requises
import flash.net.URLRequest;
import flash.display.BitmapData;
import flash.events.*;
import flash.utils.ByteArray;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.display.StageQuality;
import flash.display.Sprite;
import flash.media.Camera;
import flash.media.Video;
import flash.utils.Timer;
import flash.system.Security;
import flash.external.ExternalInterface;
import flash.display.BitmapData;
import com.adobe.images.JPGEncoder;
import Base64;
 
public class webcam extends Sprite {
    private var camera:Camera = null;
    private var buffer:BitmapData = null;
    private var interval:Number = 0;
    private var stream:String = null;
    private var video:Video = null;
 
    private var settings:Object = {
        bandwidth : 0,
        quality : 90,
        jpgEncode : 0, // 0 or 1
        jpgQuality : 60, // if encode JPG [0 - 100]
        framerate : 14,
        smoothing : false,
        deblocking : 0,
        wrapper : 'webcam', // nom de l'objet JS
        width : 320,
        height : 240,
        refresh : 700
    }
 
    public function webcam():void {
        flash.system.Security.allowDomain("*");
        stage.scaleMode = StageScaleMode.NO_SCALE;
        stage.quality = StageQuality.BEST;
        stage.align = StageAlign.TOP_LEFT;
        settings = merge(settings, this.loaderInfo.parameters);
        var id:Number = -1;
        for (var i:Number=0,l:Number=Camera.names.length; l>i; i++) {
            if (Camera.names[i] == "USB Video Class Video") {
                id = i;
                break;
            }
        }
        camera = Camera.getCamera();
        if (camera!=null) {
            if(ExternalInterface.available){
                loadCamera();
                // dialogue en provenance du JS
                ExternalInterface.addCallback("capture", capture);
                ExternalInterface.addCallback("turnOff", turnOff);
            }
        }
    }
    private function loadCamera(name:String = '0'):void {
        camera = Camera.getCamera(name); // récupération de la caméra
        camera.addEventListener(StatusEvent.STATUS, cameraStatusListener);
        camera.setMode(settings.width, settings.height, settings.framerate);
        camera.setQuality(settings.bandwidth, settings.quality);
        video = new Video(stage.stageWidth, stage.stageHeight); // sortie vidéo
        video.smoothing = settings.smoothing;
        video.deblocking = settings.deblocking;
        video.attachCamera(camera);
        stage.addChild(video);
        video.x = (stage.stageWidth - video.width) / 2;
        video.y = (stage.stageHeight - video.height) / 2;
    }
    private function cameraStatusListener(evt:StatusEvent):void {if(!camera.muted) ExternalInterface.call('camOk');}
    public function turnOff():Boolean {video.attachCamera(null); stage.removeChild(video); return true;}
    public function capture(time:Number):Boolean {
        if (camera!=null) {
            if (buffer!=null) {return false;}
            buffer = new BitmapData(settings.width, settings.height);
            var stream:Timer = new Timer(settings.refresh);
            stream.addEventListener(TimerEvent.TIMER, streaming);
            stream.start(); // envoi régulier d'images selon timer
            return true;
        }
        return false;
    }
    // envoi de l'image ligne par ligne (0) ou en JPG (else 1)
    private function streaming(e:TimerEvent):void {
        buffer.draw(video);
        if(settings.jpgEncode==0) {
            for (var i:Number = 0; settings.height>i; ++i) {
                var row:String = "";
                for (var j:Number=0; settings.width>j; ++j) {
                    row+= buffer.getPixel(j, i);
                    row+= ";";
                }
                ExternalInterface.call(settings.wrapper+'.onSave', row); // vers JS
            }
        } else {
            var jpg:ByteArray = new JPGEncoder(settings.jpgQuality).encode(buffer);
            var jpg64:String = 'data:image/jpeg;base64,' + Base64.encodeByteArray(jpg);
            ExternalInterface.call(settings.wrapper+'.onSave', jpg64); // vers JS
        }
 
    }
    // chargement des valeurs (largeur, hauteur, qualité...)
    public static function merge(base:Object, overwrite:Object):Object {
        for(var key:String in overwrite)
        if(overwrite.hasOwnProperty(key)){
            if(!isNaN(overwrite[key])) base[key] = parseInt(overwrite[key]);
            else if(overwrite[key]==='true') base[key] = true;
            else if(overwrite[key]==='false') base[key] = false;
            else base[key] = overwrite[key];
        }
        return base;
    }
}
}

Le fichier créé pèse 5,8ko. En supprimant la possibilité d'encodage JPG, il ne pèse que 2,3ko.

Télécharger le code source