Montag, 4. März 2013

Youtube Clone selber gemacht mit Javascript

Einfacher Video-Portal Server schnell selbst gemacht

NodeJS bietet mit dem Modul Express eine sehr einfache und schnelle Möglichkeit einen Webserver aufzusetzen, der auch noch sehr schnell erweitert werden kann.
Hier ein Beispiel für einen Video Portal. Das komplette System ist unabhängig von der Plattform. Ich habe die Realisierung auf Windows vorgenommen. 

Features des Servers

  • Upload Videos
  • Konvertierung von Videos in MP4 Format
  • Liste der vorhandenen Videos mit Vorschaubild
  • Abspielen der Videos in HTML5
  • Dynamisch erweiterbar
  • Statischer Server
  • Gesicherter statischer Bereich für Default Dateien
  • Dynamisches Laden von JS-Modulen in den Webserver
  • Automatisches Nachladen von JS-Modulen die sich geändert haben (updates)

Die Umsetzung des Video Portals zeigt folgende Elemente

  • Dynamische Erweiterung vom statischer Ansatz von NodeJS Server
    Einfacher Nodejs-Webserver
  • Neuen Prozess starten für die Konvertierung
  • HTTP Range-Control
  • HTML5 Video abspielen mit Javascript

Wir benötigen NodeJs und 3 Zutaten

Fertige Verzeichnisstruktur

  • /ffmpeg
  • /server
    • /www (hier liegen statische WWW Inhalte)
      • /filmkonvert
      • /upload
      • (Filme etc.)
    • /wwwtemplates
      • favicon.ico
      • videos.html (Anzeige der Videos und Upload Formular)
    • /node_modules
      • /express
      • /mime
    • server_express.js (der eigentliche Server als Javascript Datei)

Installation

Zuerst NodeJS installieren. Es gibt auch eine Version ohne Installation, dann fehlt aber "npm" für das Installieren der benötigten Module. Man kann aber nach der Installation den Modul-Ordner kopieren. Wenn man einen 100% transportable Version erstellen will, einfach zuerst die Module in einen Ordner installieren und dann diese in den eigentlichen Server-Ordner kopieren und dort auch die Standalone-Version von nodejs hinein kopieren.

Man könnte den "ffmpeg" Ordner auch in den "server" Ordner legen. ffmpeg ist eine Standalone Version. Herunterladen und Entpacken. Fertig. In der Regel sind dann 3 Dateien vorhanden. Wir benötigen eigentlich nur die ffmpeg.exe.
Dateien FFmpeg
  • ffmpeg.exe
  • ffplay.exe
  • ffprobe.exe

Code: Einfacher NodeJS Server mit Range Control

Dieser Code Auszug enthält auch Teile für ein anderes Projekt. Diese können entfernt werden, wenn man keinen Nutzen darin sieht.

 var express = require('express');  
 var app = express();  
 var PATH = require('path');  
 var FS = require('fs');  
 var MIME = require('mime');  
 var system = {  
      port:3000  
      ,tmpdir:"tmp"  
      ,wwwdir:"www"  
      ,wwwtemplatesdir:"wwwtemplates" // hier liegen die dateien zum ausliefern  
      ,uploaddir:"uploads"  
      ,zaehler:0  
      ,JSTeile:{}  
      ,args:[]  
      ,jsdir:"js"  
      ,tools:{  
           'ffmpeg':{pfad:'..\\ffmpeg\\ffmpeg.exe'}  
           }       
 }  
 checkARGs();  
 starten();  
 function starten() {  
      if (!FS.existsSync(system.uploaddir)) {  
           FS.mkdirSync(system.uploaddir);  
      }  
      if (!FS.existsSync(system.tmpdir)) {  
           FS.mkdirSync(system.tmpdir);  
      }  
      // leere die tmpdir und uploaddir  
           // auch per timeout  
      // json-callback-namen aus parameter cbf nehmen  
      app.set('jsonp callback name', 'cbf');   
      //app.use(express.logger());  
      app.use(function(req,res,next) { // mein logger  
           // req.params  
           // req.body  
           // reg.query  
           // req.files  
           // req.cookies  
           system.zaehler++;  
           res.locals.zaehler = system.zaehler;  
           console.log("["+Date2Text()+"]", req.ip+":"+req.connection.remotePort, req.host, req.protocol, req.method, req.originalUrl);  
           next();       
           });  
      // zuerst static content  
      app.use(express.bodyParser({ keepExtensions: true, uploadDir: system.uploaddir }));  
      app.use(express.cookieParser());  
      app.use(function(req, res,next){  
           var range = req.header('Range');  
           if (!range)  
                return next();  
           var file = PATH.join(system.wwwdir,req.path);  
           if (!FS.existsSync(file))  
                return next();  
       var stat = FS.statSync(file);  
       if (!stat.isFile()) return next();  
       var start = parseInt(range.slice(range.indexOf('bytes=')+6, range.indexOf('-')));  
       var end = parseInt(range.slice(range.indexOf('-')+1, range.length));  
       if (isNaN(end) || end == 0) end = stat.size-1;  
       if (start > end) return;  
       console.log("["+Date2Text()+"]",'Browser requested bytes from ' + start + ' to ' +end + ' of file ' + file);  
       var date = new Date();  
       res.writeHead(206, { // NOTE: a partial http response  
        // 'Date':date.toUTCString(),  
        'Connection':'close',  
        // 'Cache-Control':'private',  
         'Content-Type':MIME.lookup(file),  
         'Content-Length':end - start,  
        'Content-Range':'bytes '+start+'-'+end+'/'+stat.size,  
         'Accept-Ranges':'bytes',  
        // 'Server':'CustomStreamer/0.0.1',  
        'Transfer-Encoding':'chunked'  
        });  
       var stream = FS.createReadStream(file,  
        { flags: 'r', start: start, end: end});  
       stream.on("open", function (fd) {  
               stream.pipe(res);  
                });  
                stream.on("error", function(){  
                     res.end();  
                     //console.log("#"+res.locals.zaehler+" Fehler beim ausliefern der Datei");  
                });  
                stream.on("data", function(data){ // daten kommen hier vorbei  
                     res.bytezaehler += data.length;  
                     //console.log("#"+res.locals.zaehler, data.length, bytezaehler, stats.size);  
                });  
                stream.on("end", function(){  
                     res.end();  
                     //console.log("#"+res.locals.zaehler+" Datei-Auslieferung beendet. Gesendet:",res.bytezaehler);  
                });  
       });  
      app.use(express.static(system.wwwdir));  
      app.use(express.static(system.wwwtemplatesdir)); // vom system bereit gestellte elemente  
      app.use(function(req,res,next) {  
                // auswerten req  
                if (req.query.cbf && req.query.cbid) {  
                     var antwort="";  
                     if (req.query['function']) {  
                          switch(req.query['function']){  
                               case "mdirlist":  
                                    //new DynServer(req,res).zeigeDirListe(PATH.join(system.wwwdir,req.path));  
                             if (!system.DynServer)  
                               system.DynServer = new DynServer(req,res,system);  
                             return system.DynServer.zeigeDirListe(system.wwwdir);  
                                    break;  
                               case "livecam":  
                                    //new DynServer(req,res).zeigeDirListe(PATH.join(system.wwwdir,req.path));  
                             //if (!system.DynServer)  
                              // system.DynServer = new DynServer(req,res,system);  
                             //return system.DynServer.streamLiveCam();  
                                    break;  
                               default:  
                                    JSONPErrorAnworten(req,res,"function not supported");                                      
                          }  
                     }  
                }   
                if (req.query['dynserver'] || req.body['dynserver']) {  
               if (!system.DynServer)  
                 system.DynServer = new DynServer(req,res,system);  
               system.DynServer.auswerten();  
                }  
                else {  
                     next();  
                }  
           });  
      app.use(function(req,res){  
           res.send(404, 'Not found');  
           });  
      app.listen(system.port);  
      console.log("server auf ",system.port," - wwwdir:",system.wwwdir);  
 }  
 function JSONPAnworten(req,res,antwort,status){  
      if (!status)  
           status = 200;  
      res.jsonp(status, { cbid: req.query.cbid, response: antwort });       
 }  
 function JSONPErrorAnworten(req,res,antwort,status){  
      if (!status)  
           status = 200;  
      res.jsonp(status, { cbid: req.query.cbid, error: antwort });       
 }  
 function URLLadenAndRun(pfad, cbfkt) {   
      URLLaden(pfad, function(html) {  
           console.log(pfad, "geladen");  
           var vm = require('vm');  
           var script = vm.createScript(html);  
           script.runInThisContext();  
           if (cbfkt)  
                cbfkt();            
      });       
 }       
 function URLLaden(url, cbfkt) {       
      if (url.match("://")) {  
           var http;  
           if (url.match("s://")){  
                http = require('https');  
           } else {  
                http = require('http');  
           }  
           http.get(url, function(res) {  
            //console.log("Got response: " + res.statusCode);  
            var html = "";  
            res.on("data", function(chunk){  
                 html += chunk;  
            });  
            res.on("end", function(){  
                 cbfkt(html);  
            });  
            res.on("close", function(err) {  
                 cbfkt(html);  
                 console.log("Error holen "+url);  
            });  
           }).on('error', function(e) {  
            console.log("Got error: " + e.message);  
           });  
      } else {  
           var fs = require("fs");  
           fs.readFile(url, 'utf8', function(err, data){  
                if (err){  
                     console.log(err);  
                } else {  
                     cbfkt(data);  
                }  
           });             
      }  
 }  
 time = function(){  
      var t = new Date();  
      return t.getTime();   
 }  
 Date2Text = function(millisek, format) {  
      if (!millisek) {  
           var t = new Date();  
           millisek = t.getTime();  
      }  
      var d = new Date(millisek);  
      if (!format)  
           format = "%d.%m.%Y %H:%i";  
      var tage = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];  
      var monate = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dez'];  
      var formate = {'d':((d.getDate()<10)?'0'+d.getDate():d.getDate()),  
                'j':d.getDate(),'D':tage[d.getDay()],'w':d.getDate(),'m':((d.getMonth()+1<10)?'0'+(d.getMonth()+1):d.getMonth()+1),'M':monate[d.getMonth()],  
                'n':d.getMonth()+1,'Y':d.getFullYear(),'y':((d.getYear()>100)?(d.getYear().toString().substr(d.getYear().toString().length-2)):d.getYear()),  
                'H':((d.getHours()<10)?'0'+d.getHours():d.getHours()),'h':((d.getHours()>12)?(d.getHours()-12):d.getHours()),  
                'i':((d.getMinutes()<10)?'0'+d.getMinutes():d.getMinutes()),'s':((d.getSeconds()<10)?'0'+d.getSeconds():d.getSeconds())  
                }  
   for (var akey in formate) {  
     var rg = new RegExp('%'+akey, "g");  
     format = format.replace(rg, formate[akey]);  
   }  
      return format;  
 }  
 function checkARGs() {  
      if (process.argv.length > 2) {  
           system.args = process.argv;  
           for (var a=0;a<system.args.length;a++){  
                if (system.args[a].substr(0,6) == "start=") {  
                     var teile = system.args[a].substr(6).split(",");  
                     for (var b=0;b<teile.length;b++) {  
                          system.JSTeile[teile[b]] = starteJSTeil(teile[b]);  
                     }                      
                } else  
                if (system.args[a].substr(0,7) == "wwwdir=") {  
                     system.wwwdir = system.args[a].substr(7);  
                }  
           }  
      }  
 }  
 function starteJSTeil(filename) {  
      var obj = {'file':filename, 'name':filename.substr(0,filename.length-3)};  
      var pfad = PATH.join(system.jsdir,filename);  
      //obj.fkt = require("./"+pfad);  
      URLLadenAndRun(pfad, function(){  
           obj.handle = FS.watch(pfad,   
                     { persistent: false },   
                     function (event, filename) {  
                          if (filename) {  
                               console.log("Jobs Datei hat sich geändert. Neu einladen. "+event+":"+filename)  
                               if (event == "rename"){  
                                    //  
                               }  
                          }  
                          if (event == "change") {  
                               //obj.fkt = require("./"+pfad);  
                               URLLadenAndRun(pfad);            
                          }  
                });  
           obj.handle.on("error", function (e){console.log(pfad,e);});  
      });   
      return obj;  
 }  
 var DynServer = function (req,res,system) {  
      // new DynServer().auswerten();  
      this.req = req;  
      this.res = res;       
      this.system = system;  
      this.paras = {};  
     this.livecamHandle = null;  
      if (req.query) {  
           for (key in req.query) {  
                this.paras[key] = req.query[key];  
           }  
      }  
      if (req.body) {  
           for (key in req.body) {  
                this.paras[key] = req.body[key];  
           }  
      }  
      this.zeigeDirListe = function(pfad) {  
           var crypto = require('crypto');  
           var self = this;  
           FS.readdir(pfad, function(err,files) {  
                if (err){  
                     return JSONPErrorAnworten(self.req, self.res, "no files");  
                }  
                var nfiles = [];  
                for (var a=0;a<files.length;a++){  
                     if (files[a].substr(0,1) == ".")  
                          continue;  
                     nfiles.push({'src':files[a],'md5':crypto.createHash('md5').update(files[a]).digest("hex")});  
                }  
                JSONPAnworten(self.req, self.res, nfiles);  
           });  
      }  
      this.filmkonvertieren = function(pfad, cbfkt) {  
           console.log("["+Date2Text()+"] starting converting", pfad);  
           var output = PATH.basename(pfad);  
           output = PATH.join(PATH.dirname(pfad), output.substr(0,output.lastIndexOf("."))+"-KONVERTING");  
           console.log("["+Date2Text()+"] starting converting", pfad, output);  
           var child_process = require('child_process');  
           var ffmpeg_pfad = this.system.tools.ffmpeg.pfad;  
           // http://videoencoding.websmith.de/encoding-praxis/linux-ffmpeg-encoding.html  
           var params = ["-y", "-i", pfad, "-acodec", "libmp3lame", output+".mp4"];  
           console.log("["+Date2Text()+"]", ffmpeg_pfad, params.join(" "));  
           childProcess = child_process.spawn(ffmpeg_pfad, params);  
           childProcess.stderr.on('data', function (data){  
                // hier kommen auch die log-meldungen an  
 //               console.log('FFMPEG-Log: ' + data);  
           });  
           childProcess.stdout.on('data', function (data) {  
 //            console.log('FFMPEG: ' + data);  
           });  
           childProcess.on('exit', function (code) {                 
             console.log("["+Date2Text()+"] FFMPEG fertig alles ok:",code);  
             if (code != 0) {  
                  // fehler mit code nummer  
                  FS.unlink(output+".mp4");  
                  if (cbfkt)  
                       cbfkt(code, pfad);  
             } else {  
                  // kopieren  
                  FS.rename(output+".mp4", pfad, function(err) {  
                       if (err){  
                            console.log("["+Date2Text()+"] Konvertierung fehlgeschlagen");  
                            FS.unlink(output+".mp4");  
                            if (cbfkt)  
                                 cbfkt(code, pfad);  
                            return;  
                          }  
                          console.log("["+Date2Text()+"] Konvertierung fertig");  
                       if (cbfkt)  
                            cbfkt(code, pfad);  
                     });  
                }  
           });  
      }  
      this.upload = function () {  
           console.log("["+Date2Text()+"] upload starten",req.path);  
           // handle upload  
           if (req.files && !isEmptyObject(req.files)) {  
                //console.log(req.files);  
                var self = this;  
                FS.exists(PATH.join(self.system.wwwdir, req.path), function(exists) {  
                     if (!exists) {  
                          return res.send(403, 'upload does not folder exists '+req.path);  
                          return;  
                     }  
                     var dateien = [];  
                     for (var key in req.files) {  
                          var file = req.files[key];  
                          if (!file.filename || file.filename == "") {  
                               FS.unlink(file.path);  
                               //JSONPErrorAnworten(self.req, self.res, "file upload has no name");  
                               res.send(403, "file upload has no name");  
                               return;  
                          }  
                          // upload path  
                          var datei = PATH.join(req.path, file.filename);   
                          var pfad = PATH.join(self.system.wwwdir, datei);  
                          try {  
                               FS.renameSync(file.path, pfad);  
                               console.log("["+Date2Text()+"] upload:",file.length, pfad);  
                          } catch(e) {  
                               console.log(e);  
                               console.log("["+Date2Text()+"] Fehler beim upload verschieben - versuche tempupload zu entfernen");  
                               FS.unlink(file.path);  
                               continue;  
                          }  
                          dateien.push(datei);  
                          // spezielles wenn jemand in filmconvert was hinterlegt  
                          if (req.path == "/filmkonvert" || req.path == "/filmkonvert/") {  
                               // starte film verarbeitung  
                               switch(file.type.toLowerCase()){  
                                    case "video/mp4":  
                                    case "video/mov":  
                                    case "video/avi":  
                                    case "video/mpg":  
                                    case "video/ogg":  
                                    case "video/mpeg4":  
                                    case "video/wmv":  
                                    default:  
                                         if (file.type.substr(0,6) == "video/"){  
                                              self.filmkonvertieren(pfad, function(code, pfad) {  
                                                   if (code != 0) {  
                                                        // error  
                                                        FS.unlink(pfad);   
                                                        return;  
                                                   }  
                                                   var npfad = PATH.join(self.system.wwwdir, PATH.basename(pfad).replace(PATH.extname(pfad), ".mp4"));  
                                                   FS.rename(pfad, npfad, function(err){  
                                                        if (err) {  
                                                             console.log("["+Date2Text()+"]", "Error not moved to", npfad);  
                                                             FS.unlink(pfad);  
                                                        } else {  
                                                             console.log("["+Date2Text()+"]", "moved", npfad);       
                                                        }  
                                                   });  
                                              });  
                                         }                                     
                               }  
                          }  
                     }  
                     if (self.paras.data && self.paras.data.redirect) {  
                          return res.redirect(self.paras.data.redirect);   
                     } else  
                     if (dateien.length > 0) {  
                          //return JSONPAnworten(self.req, self.res, dateien);  
                          return res.send(200, 'upload ok '+JSON.stringify(dateien));                                                         
                     } else {  
                          return res.send(403, 'problems with upload');  
                     }  
                });  
           } else {  
                //return JSONPErrorAnworten(self.req, self.res, "no files uploaded");  
                return res.send(403, 'no files uploaded');  
           }       
      }  
      this.auswerten = function(){  
           if (!this.paras['func'])  
                return JSONPErrorAnworten(this.req, this.res, "no func recognised");       
           switch(this.paras['func']) {  
                case "upload":  
                     return this.upload();  
                     break;  
                default:  
                     return JSONPErrorAnworten(this.req, this.res, "func not supported");  
           }  
           // keine antwort gesendet, also fehler bei den clientdaten  
           return JSONPErrorAnworten(this.req, this.res, "func error");  
      }  
      return this;  
 }  
 function isEmptyObject(obj) {  
   // This works for arrays too.  
   for(var name in obj) {  
     return false  
   }  
   return true  
 }  

Code: HTML Webseite für Video-Portal

Als Beispiel wird auch eine externe Quelle eingebunden. Für die Kommunikation mit dem Server nutze ich APIcalls (Hier lesen Sie wie man APICalls aufbaut, wenn weiter Infos gewünscht)

 <!DOCTYPE html>  
 <html>  
 <head>  
 <title>aus static template verzeichnis</title>  
 <script type="text/javascript">  
 var videos = [  
      {'src':'video2.mp4',"type":'video/mp4',md5:'m1'}  
      ,{'src':'output.mp4',"type":'video/mp4',md5:'m2'}  
      ,{'src':'http://podfiles.zdf.de/podcast/zdf_podcasts/130108_h19_414k_p20v9.mp4?2013-01-0919-08','type':'video/mp4',md5:'m3'}  
 ];  
 var lastpos = 0;  
 function starten() {       
      // lade die Videoliste  
      ladeFilmliste(function(vids) {  
           var v = document.getElementById("v");       
           v.addEventListener("loadstart", function() {  
                document.getElementById("info").innerHTML = "Loading... "+v.src;  
                document.getElementById("navelem").style.visibility = "hidden";  
                v.controls = false;  
           });  
           v.addEventListener("canplay", function() {  
                v.controls = true;  
                document.getElementById("navelem").style.visibility = "visible";  
                var zeit = Math.round(v.duration);  
                var minuten = Math.floor(zeit/60);  
                var sek = zeit%60;   
                if (sek < 10)  
                     sek = "0"+sek;  
                document.getElementById("info").innerHTML = minuten+":"+sek+" ["+v.src+"] - "+v.offsetWidth+"x"+v.offsetHeight;  
           });             
           v.addEventListener("ended", function(e) {  
                //console.log(e);  
              if (v.loop){  
                   abspielen(v);  
                } else {  
                     next();  
                }  
        });  
           //v.autoplay = true;  
           zeigeFilmListe();  
           lastpos--;  
           next(true);  
      });  
      window.setTimeout(aktualisiereFilmListe, 60000);       
      window.onresize = resize;  
 }  
 function resize() {  
      var v = document.getElementById("film_liste");  
      var vc = document.getElementById("film_liste_container");  
      var vpc = document.getElementById("filmp_container");  
      var hoehe = v.offsetHeight;  
      var fhoehe = window.innerHeight-60-40;  
      if (hoehe > fhoehe) {  
           vc.style.overflow = "hidden";  
           vc.style.overflowY = "scroll";  
           vc.style.height = fhoehe+"px";  
           vpc.style.overflow = "hidden";  
           vpc.style.overflowY = "scroll";  
           vpc.style.height = fhoehe+"px";  
      } else {  
           vc.style.overflow = "";  
           vc.style.overflowY = "";  
           vc.style.height = hoehe+"px";  
           vpc.style.overflow = "";  
           vpc.style.overflowY = "";  
           vpc.style.height = hoehe+"px";  
      }  
      var vp = document.getElementById("v");  
      //var ratio = v.offsetWidth / v.offsetHeight;  
      var breite = document.getElementById("info").offsetWidth-30;  
      vp.style.width = breite+"px";  
 }  
 function aktualisiereFilmListe() {  
      ladeFilmliste(function (vids){  
           zeigeFilmListe();  
           window.setTimeout(aktualisiereFilmListe, 60000);       
      });  
 }  
 function ladeFilmliste(cbfkt) {  
      apicall("request/?function=mdirlist", function(h){  
           if (h.object.error) {  
                alert(h.object.error);  
           } else {  
                initialisieren(h.object.response);  
                if (cbfkt) {  
                     cbfkt(h.object.response);  
                }  
           }  
      });  
 }  
 function initialisieren(filme) {  
      if (filme) {  
           var vf = filme;   
           for (var a=0;a<vf.length;a++) {  
                var inlist = false;  
                for (var key in videos) {  
                     if (vf[a].src == videos[key].src) {  
                          inlist = true;  
                          break;  
                     }  
                }  
                if (inlist)  
                     continue;  
                if (vf[a].src.toLowerCase().match(/\.mp4$/)) {  
                     videos.push({'src':vf[a].src, "type":'video/mp4',md5:vf[a].md5});  
                } else  
                if (vf[a].src.toLowerCase().match(/\.ogg$/)) {  
                     videos.push({'src':vf[a].src, "type":'video/ogg',md5:vf[a].md5});  
                } else  
                if (vf[a].src.toLowerCase().match(/\.webm$/)) {  
                     videos.push({'src':vf[a].src, "type":'video/webm',md5:vf[a].md5});  
                }  
           }  
      }       
 }  
 function zeigeFilmListe() {     
      var elem = document.getElementById("film_liste");       
      var text = "";  
      var vs = document.getElementsByTagName("video");  
      for (var a=0;a<videos.length;a++) {  
           if (document.getElementById("p_"+videos[a].md5))  
                continue;  
           var inlist = false;  
           for (var key in vs) {  
                if (vs[key].src == videos[a].src){  
                     inlist = true;  
                     break;  
                }  
           }  
           if (inlist)  
                continue;  
           elem.appendChild(erstelleThumb(videos[a]));  
      }       
      resize();  
 }  
 function erstelleThumb(video) {  
      var elk = document.createElement("source");  
      elk.src = video.src;  
      elk.type = video.type;            
      var el = document.createElement("video");  
      el.appendChild(elk);  
      el.preload = "metadata";  
      el.style.width = "100%";  
      el.id = "p_"+video.md5;  
      el.src = video.src;  
      el.addEventListener("click", function(e){playMD5(video.md5);});  
      el.addEventListener("canplay", function() {  
                el.style.borderStyle = "solid";  
                el.style.borderColor = "white";  
                el.style.borderWidth = "medium";  
                el.currentTime = el.duration*.1;  
                el.play();  
                el.pause();  
           });  
      return el;  
 }  
 function play(btnelem) {            
      var v = document.getElementById("v");  
      if (v.paused) {  
           abspielen(v);  
           btnelem.innerHTML = "pause";  
      } else {  
           v.pause();  
           btnelem.innerHTML = "play";  
      }  
 }  
 function playMD5(md5) {  
      //debugger;  
      for (var a=0;a<videos.length;a++) {  
           if (videos[a].md5 == md5) {  
                playPos(a);  
                break;  
           }  
      }  
 }  
 function playPos(pos){       
      lastpos = pos-1;  
      next();  
 }  
 function next(dontplay) {  
      lastpos++;  
      if (lastpos >= videos.length)  
           lastpos = 0;  
      var kind = document.createElement("source");  
      kind.src = videos[lastpos].src;   
      kind.type = videos[lastpos].type;  
      var elem = document.getElementById("v");  
      v.replaceChild(kind,v.firstChild);       
      v.preload = "metadata";  
      v.src = videos[lastpos].src;  
      v.type = videos[lastpos].type;  
      if (!dontplay)  
           abspielen(v);            
 }  
 function abspielen(v) {  
      v.play();  
      for (var a=0;a<videos.length;a++){  
           if (document.getElementById("p"+a)) {  
                var ee = document.getElementById("p"+a);  
                var srcname = v.src.replace(/\\/g,'/').replace( /.*\//, '' );  
                if (videos[a].src == srcname) {  
                     ee.style.borderColor = "green";  
                } else {  
                     ee.style.borderColor = "white";  
                }                 
           }  
      }  
 }  
 function loop(btnelem) {  
      var v = document.getElementById("v");  
      if (v.loop) {  
           btnelem.innerHTML = "loop";  
      } else {  
           btnelem.innerHTML = "stop loop";  
      }  
      v.loop = !v.loop;  
 }  
 function apicall(url, onSuccess, onError, onErrorTimeout, urlid) {  
      if (typeof(system) == "undefined")  
           system = {};  
      if (!system['dscript'])  
           system['dscript'] = {'id':1, 'cb':{}, 'urls':{}};            
   var id = system["dscript"]["id"];  
   system["dscript"]["cb"][id] = {"url": url, "onSuccess": onSuccess, "onError": onError, "time": new Date().getTime()};  
   var refurl = url;  
   if (!urlid)  
        urlid = refurl  
   system["dscript"]["urls"][urlid] = id; // für die statischen URLs  
   var head  = document.getElementsByTagName("head")[0];  
   var script = document.createElement('script');  
   script.id  = 'obj_dscript_'+id;  
   if (id > 36000 && !system["dscript"]["cb"][1])  
        system["dscript"]["id"] = 1;       
   script.type = 'text/javascript';    
      var srcurl = url;  
      if (srcurl.indexOf("?")<1)  
           srcurl += "?"     
   script.src = srcurl+"&cbf=SN_APIDispatcher&cbid="+id;  
   var d = new Date();  
   script.src += '&t='+d.getTime();  
   head.appendChild(script);  
   var timeout = 5000; // 5 sekunden  
   if (onErrorTimeout)  
        timeout = onErrorTimeout;   
      var errorcheck = setTimeout("SN_APIErroLoadCheck('"+id+"')", timeout);  
      system["dscript"]["cb"][id]['errorcheck'] = errorcheck;    
   system["dscript"]["id"]++;  
 }    
 function SN_APIDispatcher(result) {  
      var id = 0;  
      if (result.cbid) {  
           id = result.cbid;  
      } else {  
           id = result.id;  
      }       
   if (system["dscript"]["urls"][id])  
    id = system["dscript"]["urls"][id];  
   if (!document.getElementById('obj_dscript_'+id))  
    return false;  
   if (system["dscript"]["cb"][id])  
        system["dscript"]["cb"][id]['dontkill'] = true;  
   if (system["dscript"]["cb"][id] && system["dscript"]["cb"][id].errorcheck)   
        clearTimeout(system["dscript"]["cb"][id].errorcheck);  
   var old = document.getElementById('obj_dscript_'+id);  
   if (old != null) {  
     old.parentNode.removeChild(old);  
     delete old;  
   }  
   var oobj = null;  
   if(typeof result == 'function')  
    result = result();  
   else if(typeof result == 'object')   
    oobj = result;   
   var obj = {"responseText": result, "object": oobj};  
   if (system["dscript"]["cb"][id])  
        system["dscript"]["cb"][id].onSuccess(obj); // errorload could have killed it already  
   delete(system["dscript"]["cb"][id]);    
 }  
 function SN_APIErroLoadCheck(apicallid){                
      if (!system["dscript"]["cb"][apicallid])  
           return false;  
      var objekt = system["dscript"]["cb"][apicallid];  
      if (system["dscript"]["cb"][apicallid]['dontkill'])  
           return false;  
      // immernoch da, also wahrscheinlich fehler aufgetreten       
      if (objekt.onError)  
           objekt.onError(apicallid);       
      delete(system["dscript"]["cb"][apicallid]);  
 }  
 window.onload=starten;  
 </script>  
 </head>  
 <body>  
 <div id="navelem" style="visibility:hidden;">  
 <button onclick="play(this)">play</button>  
 <button onclick="loop(this)">loop</button>  
 <button onclick="next()">next</button>  
 </div>  
 <div>  
      <div style="width:80%;float:left;" id="filmp_container">  
           <video width="100%" id="v">Your browser does not support the video tag.</video>  
           <div id="info"></div>  
           <div style="margin:10px;border:medium solid orange;">  
                Upload to /  
                <form action="/" enctype="multipart/form-data" method="post">  
                <input type=hidden name="dynserver" value="1">  
                <input type=hidden name="func" value="upload">  
                <input type=file name="datei">  
                <br>  
                <input type=submit>  
                </form>  
           </div>  
           <div style="margin:10px;border:medium solid orange;">  
                Upload und Film konvertieren /filmkonvert  
                <form action="/filmkonvert" enctype="multipart/form-data" method="post">  
                <input type=hidden name="dynserver" value="1">  
                <input type=hidden name="data[redirect]" value="/videos2.html">  
                <input type=hidden name="func" value="upload">  
                <input type=file name="datei">  
                <br>  
                <input type=submit>  
                </form>  
           </div>  
      </div>  
      <div style="width:20%;float:left;" id="film_liste_container">  
           <div id="film_liste" style="text-align:center;margin:5px;margin-top:0;"></div>  
      </div>  
      <div style="clear:both;"></div>  
 </div>  
 <br>// http://www.w3schools.com/tags/ref_av_dom.asp  
 </body>  
 </html>  

Serverablauf

Wenn eine Upload erfolgt, prüft das Skript, ob es in den Konvertierungsordner gelegt werden soll. Hier wird auch der Dateityp bestimmt. Die Datei wird dann in den Konvertierungsordner verschoben. Die Konvertierung erfolgt in der Methode: filmkonvertieren.

Konvertierung

Die Konvertierung erfolgt über einen eigenen Prozess, da dies dauern kann. Der Prozess ruft ffmpeg mit entsprechenden Parameter auf und wartet bis die Konvertierung beendet ist. Danach wird der fertige Film in dem WWW Ordner verschoben.

FFmpeg Parameter (kann man sicherlich optimieren, aber für unsere Zwecke ausreichend):
  • -y
  • -i FILMPFAD
  • -acodec libmp3lame
  • FILMPFADNEU.mp4

Portalablauf

Die Filme werden über HTML5 und Javascript aufgelistet. Das Thumbnail wird durch das Anzeigen eines Bildes innerhalb des Film realisiert. Also durch Start und Pause der HTML5 Video Funktion. Das kann und sollte optimiert werden. 

Die Filme werden in einem Javascript Objekt gelistet. Damit kann man auch externe Filme in die Liste aufnehmen. Tada: Eine Filmportalseite auch ohne eigenen Server, als Bonus. Der Upload erfolgt über das HTML Formular.

Fazit

Das ist es. Eigentlich nur 2 Dateien mit etwas Code.

Mit diesem dynamischen Ansatz kann man nun beliebig weiter machen. Denkbar wäre auch ein Aufruf von anderen Interpretern und damit auch die Realisierung eines PHP-Moduls in NodeJS. Interessant ist die Möglichkeit neue Prozesse zu starten und damit mehr Flexibilität und Effizienz zu erhalten.

Viel Spass damit.
Saso Nikolov

Freitag, 1. Februar 2013

Ein einfacher NodeJS Webserver mit Range-Control

Ein einfacher Webserver - ideal für Testzwecke

Oft braucht man eine Webserver für kleine statische Inhalte. Apache und Co sind zu gross. Doch sollte der Server auch die Möglichkeit bieten grosse Videos abspielen zu können. Wir brauchen also die Möglichkeit den Range Header auszuwerten.

Benötigte Module

  • Express
  • Mime

Features eingebaut

Der Server liefert Inhalte aus einem statischen Verzeichnis. Wenn die Datei dort nicht liegt, wird ein weiteres verstecktes statisches Verzeichnis geprüft. Damit kann man Standardinstallationen ausliefern, bis der Kunde im echten web-Ordner die entsprechenden Dateien hinterlegt. 

Dazu kann man auch eine dynamische Abarbeitung von Anfragen anstossen. Dazu dient die Klasse DynServer, die noch durch entsprechenden Code ausgebaut werden muss. Im Beispiel kann man die Vorgehensweise gut erkennen.

Was kann der Webserver?

  • HTTP-Range (keine Unterstützung für Multi-Range)
  • Upload
  • Auslieferung statischer Inhalte
  • Auslieferung von unsichtbaren statischen Inhalten
  • Starten von dynamischen Prozessen

NodeJS und Express und Mime

Diese Variante benötigt das Express und Mime Modul. Damit hat man weniger Code, muss jedoch in der Lage sein, dieses Modul auf dem Server installieren zu können.

 var express = require('express');  
 var app = express();  
 var PATH = require('path');  
 var FS = require('fs');  
 var MIME = require('mime');  
 var system = {  
     port:3000  
     ,tmpdir:"tmp"  
     ,wwwdir:"www"  
     ,wwwtemplatesdir:"wwwtemplates" // hier liegen die dateien zum ausliefern  
     ,uploaddir:"uploads"  
     ,zaehler:0  
     ,JSTeile:{}  
     ,args:[]  
 }  
 checkARGs();  
 starten();  
 function starten() {  
     if (!FS.existsSync(system.uploaddir)) {  
         FS.mkdirSync(system.uploaddir);  
     }  
     if (!FS.existsSync(system.tmpdir)) {  
         FS.mkdirSync(system.tmpdir);  
     }  
     // leere die tmpdir und uploaddir  
         // auch per timeout  
     // json-callback-namen aus parameter cbf nehmen  
     app.set('jsonp callback name', 'cbf');   
     //app.use(express.logger());  
     app.use(function(req,res,next) { // mein logger  
         // req.params  
         // req.body  
         // reg.query  
         // req.files  
         // req.cookies  
         system.zaehler++;  
         res.locals.zaehler = system.zaehler;  
         console.log("["+Date2Text()+"]", req.ip+":"+req.connection.remotePort, req.host, req.protocol, req.method, req.originalUrl);  
         next();      
         });  
     // zuerst static content  
     app.use(express.bodyParser({ keepExtensions: true, uploadDir: system.uploaddir }));  
     app.use(express.cookieParser());  
     app.use(function(req, res,next){  
         var range = req.header('Range');  
         if (!range)  
             return next();  
         var file = PATH.join(system.wwwdir,req.path);  
         if (!FS.existsSync(file))  
             return next();  
      var stat = FS.statSync(file);  
      if (!stat.isFile()) return next();  
      var start = parseInt(range.slice(range.indexOf('bytes=')+6, range.indexOf('-')));  
      var end = parseInt(range.slice(range.indexOf('-')+1, range.length));  
      if (isNaN(end) || end == 0) end = stat.size-1;  
      if (start > end) return;  
      console.log("["+Date2Text()+"]",'Browser requested bytes from ' + start + ' to ' +end + ' of file ' + file);  
      var date = new Date();  
      res.writeHead(206, { // NOTE: a partial http response  
       // 'Date':date.toUTCString(),  
       'Connection':'close',  
       // 'Cache-Control':'private',  
        'Content-Type':MIME.lookup(file),  
        'Content-Length':end - start,  
       'Content-Range':'bytes '+start+'-'+end+'/'+stat.size,  
        'Accept-Ranges':'bytes',  
       // 'Server':'CustomStreamer/0.0.1',  
       'Transfer-Encoding':'chunked'  
       });  
      var stream = FS.createReadStream(file,  
       { flags: 'r', start: start, end: end});  
      stream.on("open", function (fd) {  
             stream.pipe(res);  
             });  
             stream.on("error", function(){  
                 res.end();  
                 //console.log("#"+res.locals.zaehler+" Fehler beim ausliefern der Datei");  
             });  
             stream.on("data", function(data){ // daten kommen hier vorbei  
                 res.bytezaehler += data.length;  
                 //console.log("#"+res.locals.zaehler, data.length, bytezaehler, stats.size);  
             });  
             stream.on("end", function(){  
                 res.end();  
                 //console.log("#"+res.locals.zaehler+" Datei-Auslieferung beendet. Gesendet:",res.bytezaehler);  
             });  
      });  
     app.use(express.static(system.wwwdir));  
     app.use(express.static(system.wwwtemplatesdir)); // vom system bereit gestellte elemente  
     app.use(function(req,res,next) {  
             // auswerten req  
             if (req.query['dynserver'] || req.body['dynserver']) {  
           if (!system.DynServer)  
             system.DynServer = new DynServer(req,res,system);  
           system.DynServer.auswerten();  
             }  
             else {  
                 next();  
             }  
         });  
     app.use(function(req,res){  
         res.send(404, 'Not found');  
         });  
     app.listen(system.port);  
     console.log("server auf ",system.port," - wwwdir:",system.wwwdir);  
 }  
 function JSONPErrorAnworten(req,res,antwort,status){  
     if (!status)  
         status = 200;  
     res.jsonp(status, { cbid: req.query.cbid, error: antwort });      
 }  
 // hilfsfunktionen  
 time = function(){  
     var t = new Date();  
     return t.getTime();   
 }  
 Date2Text = function(millisek, format) {  
     if (!millisek) {  
         var t = new Date();  
         millisek = t.getTime();  
     }  
     var d = new Date(millisek);  
     if (!format)  
         format = "%d.%m.%Y %H:%i";  
     var tage = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];  
     var monate = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dez'];  
     var formate = {'d':((d.getDate()<10)?'0'+d.getDate():d.getDate()),  
             'j':d.getDate(),'D':tage[d.getDay()],'w':d.getDate(),'m':((d.getMonth()+1<10)?'0'+(d.getMonth()+1):d.getMonth()+1),'M':monate[d.getMonth()],  
             'n':d.getMonth()+1,'Y':d.getFullYear(),'y':((d.getYear()>100)?(d.getYear().toString().substr(d.getYear().toString().length-2)):d.getYear()),  
             'H':((d.getHours()<10)?'0'+d.getHours():d.getHours()),'h':((d.getHours()>12)?(d.getHours()-12):d.getHours()),  
             'i':((d.getMinutes()<10)?'0'+d.getMinutes():d.getMinutes()),'s':((d.getSeconds()<10)?'0'+d.getSeconds():d.getSeconds())  
             }  
   for (var akey in formate) {  
     var rg = new RegExp('%'+akey, "g");  
     format = format.replace(rg, formate[akey]);  
   }  
     return format;  
 }  
 // analysieren der consolen Parameter  
 function checkARGs() {  
     if (process.argv.length > 2) {  
         system.args = process.argv;  
         for (var a=0;a<system.args.length;a++){  
             if (system.args[a].substr(0,7) == "wwwdir=") {  
                 system.wwwdir = system.args[a].substr(7);  
             }  
         }  
     }  
 }  
 var DynServer = function (req,res,system) {  
     this.req = req;  
     this.res = res;      
     this.system = system;  
     this.paras = {};  
     if (req.query) {  
         for (key in req.query) {  
             this.paras[key] = req.query[key];  
         }  
     }  
     if (req.body) {  
         for (key in req.body) {  
             this.paras[key] = req.body[key];  
         }  
     }  
     this.upload = function () {  
         console.log("["+Date2Text()+"] upload starten",req.path);  
         // handle upload  
         if (req.files && !isEmptyObject(req.files)) {  
             //console.log(req.files);  
             var self = this;  
             FS.exists(PATH.join(self.system.wwwdir, req.path), function(exists) {  
                 if (!exists) {  
                     return res.send(403, 'upload does not folder exists '+req.path);  
                     return;  
                 }  
                 var dateien = [];  
                 for (var key in req.files) {  
                     var file = req.files[key];  
                     if (!file.filename || file.filename == "") {  
                         FS.unlink(file.path);  
                         res.send(403, "file upload has no name");  
                         return;  
                     }  
                     // upload path  
                     var datei = PATH.join(req.path, file.filename);   
                     var pfad = PATH.join(self.system.wwwdir, datei);  
                     try {  
                         FS.renameSync(file.path, pfad);  
                         console.log("["+Date2Text()+"] upload:",file.length, pfad);  
                     } catch(e) {  
                         console.log(e);  
                         console.log("["+Date2Text()+"] Fehler beim upload verschieben - versuche tempupload zu entfernen");  
                         FS.unlink(file.path);  
                         continue;  
                     }  
                     dateien.push(datei);  
                 }  
                 if (self.paras.data && self.paras.data.redirect) {  
                     return res.redirect(self.paras.data.redirect);   
                 } else  
                 if (dateien.length > 0) {  
                     return res.send(200, 'upload ok '+JSON.stringify(dateien));                                              
                 } else {  
                     return res.send(403, 'problems with upload');  
                 }  
             });  
         } else {  
             return res.send(403, 'no files uploaded');  
         }      
     }  
     this.auswerten = function(){  
         if (!this.paras['func'])  
             return JSONPErrorAnworten(this.req, this.res, "no func recognised");      
         switch(this.paras['func']) {  
             case "upload":  
                 return this.upload();  
                 break;  
             default:  
                 return JSONPErrorAnworten(this.req, this.res, "func not supported");  
         }  
         // keine antwort gesendet, also fehler bei den clientdaten  
         return JSONPErrorAnworten(this.req, this.res, "func error");  
     }  
     return this;  
 }  
 function isEmptyObject(obj) {  
   // This works for arrays too.  
   for(var name in obj) {  
     return false  
   }  
   return true  
 }  

Viel Spass damit
Saso Nikolov


Freitag, 11. Januar 2013

Prozesse optimieren muss analytisch angegangen werden

Das Kaffeeverkaufen optimieren.

Was sind Prozesse?

Ich rede hier von Abläufen, welche immer wieder genutzt werden. Da diese in der Regel sehr oft genutzt werden, können Optimierungen auf lange Sicht viel bringen. Oft nicht nur (Verarbeitungs)Zeit, sondern durch die gesteigerte Geschwindigkeit auch mehr Geld, in Form von mehr Kunden oder geringere Kosten.

Oft wird argumentiert, dass dieser Prozess so klein ist, dass eine Optimierung nicht lohnt. Wenn viele kleine Prozesse hintereinander geschaltet sind, dann ist die Summe der Verzögerungen oft deutlich spürbar.

Ich möchte hier über Arbeitsprozesse sprechen, obwohl dies genauso gut auch auf das Programmieren, bzw auch Quellcode angewendet werden kann. Jedoch ist meine Sprache auf Arbeitsprozesse ausgelegt.

Warum gibt es so viele unoptimierte Arbeitsprozesse?

Weil sich die Manager, Eigentümer und Chefs nicht darum kümmern!
Jeden Tag wird irgendwo eine Hilfskraft eingestellt. Man überlässt diese Person sich selbst. Oft werden auch die Tätigkeiten nicht richtig eingeschätzt und man erwartet vom Mitarbeiter, dass er das beherrschen soll.
Resultat: Frust beim Mitarbeiter, Frust beim Kunden. 
Auf lange Sicht: Verlust der Kunden und damit Rückgang vom Umsatz.
Gerade bei gut gelegenen gastronomischen Betrieben kann man das sehr gut sehen. Die Betreiber scheinen nicht das verschwendetet Potenzial zu sehen. Handlungsbedarf scheint auch nicht zu geben, da durch die Lage immer genug neue Kunden kommen. Die vergraulten vergisst man da schnell.

Beispiel Hilfskraft Bedienung

Eine Bar/Restaurant stellt eine Hilfskraft als Bedienung ein:
Kandidatin: Hallo, ich würde gerne als Aushilfe hier arbeiten
Besitzer: Du passt optisch gut zu uns. Hier ist Block, Stift und Kassenbeutel

Dies ist natürlich total überzogen, aber doch schrecklich nahe an der Realität.
Man erwartet dass Sie eine Glas von der Bar zum Kunden bringen kann und damit fällt dann oft einen kleine Schulung aus. 
Die Schulung ist aber enorm wichtig. Für alle. Für den Mitarbeiter, damit er sicherer wird und damit Stress abbaut. Den Kunden, der eine optimierte Bestellung aufgeben kann und auch prompt beliefert wird. Der Betreiber, der sich sicher sein kann, dass seine Firmenphilosophie beim Mitarbeiter angekommen ist.

Aus Sicht der Mitarbeiterin:
  • Stress (Gerade am Anfang, wenn alles neu ist)
  • Angst
    • Kenn ich alle Produkte?
    • Wie gehe ich mit Sonderwünsche um?
    • Wie erfasse ich die Bestellung
      • Zuerst Getränke
      • Zuerst Essen
    • Was ist Firmenphilosophie?
      • Schnelle Lieferung
      • Persönliches 
        • Zeit für Smalltalk verzögert Auslieferungsprozess
        • Zeit für Smalltalk verzögert neue Bestellannahme
      • Gutes Essen
        • Kann ich die Waren richtig beschreiben?
    • Gibt es optimierte Laufwege
      • Wie werden Kundenzurufen bearbeitet
        • Vom eigenem Tischbereich
        • Vom fremden Tischbereich
  • Unsicherheit
    • Alles ist neu
    • Kollegen
    • Zusammenhänge
    • Abläufe
Das alles macht es dem neuen Mitarbeiter schwer optimale Leistung zu bringen. Hier ist ganz klar: 
Schlechte Mitarbeiter kommen durch schlechte Chefs.

Am Anfang war das Wort

Zu Beginn erfolgt die Analyse der vorhandenen Prozesse. Wir erfassen den Ablauf. Dies erfolgt am Besten von aussen. Danach können verschiedene Testvarianten ausprobiert werden und das Geschehen notiert werden. Mitarbeiter sind teilweise so tief drin, dass bestimmte Abfolgen nicht objektiv durch diese erfasst werden können.

Als Beispiel für meine weiteren Ausführungen dient mir ein Imbiss, der Käsebrötchen, Salamibrötchen und Kaffee verkauft. Damit ist es einfach gehalten und ich kann die Problematik anschaulich darstellen.
Verkaufstheke mit 4 Bereichen: Wurst, Kasse & Bestellannahme, Kaffee und Käse

Mitarbeiter befragen

Nach dem ersten Erfassen von aussen, hat man schon enorm viele Eindrücke gewinnen können. Hier kann man aber noch nicht sofort mit der Optimierung beginnen. Denn vielleicht hat eine umständlich wirkenden Prozedur einen Grund, den man von aussen nicht erfassen kann.

Bevor die Ergebnisse veröffentlicht und damit in einer Runde mit den Mitarbeitern besprochen werden können, müssen die Mitarbeiter über die Abläufe befragt werden. Hier ist es auch wichtig die Mitarbeiter ins Boot zu holen. Diese müssen das Gefühl erhalten, dass es nicht darum geht, deren schlechte Arbeit zu identifizieren. Man muss unbedingt darauf Wert legen, dass hier der Mitarbeiter als Experte, der direkt mit dem Ablauf in Berührung kommt, die Beste Optimierung beisteuern kann.
Wir brauchen die Mitarbeiter. Wir brauchen diese als Optimierungsexperten und so sollen Sie sich verstanden fühlen. 

Durch gezielte Fragen über das Beobachtete, kann man nun die Liste der Optimierungen anpassen. Diese dient dann als Ausgangspunkt für das Brainstorming und die Gruppendiskussionen.

Ziel ist ein verbesserter Ablauf

Zuerst muss man die Zielsetzung auch erklären. Folgende Fragen müssen unbedingt jedem klar sein, damit der Grund für eine Optimierung geschaffen wird. 
  • Was bringt eine Optimierung?
  • Wieso verbessert die Optimierung das Arbeitsklima
  • Wieso brauchen wir zufriedene Kunden
  • Wieso ist Umsatz wichtig?

Ziel: Optimierung grundsätzlich

Alle müssen verstehen, dass eine Optimierung nicht Rationalisierung bedeutet! Es geht nicht darum Mitarbeiter einzusparen, sondern mehr für weniger heraus zuholen. Dabei ist mit "mehr" die Effizienz und mit "weniger" die Anstrengung gemeint.

Optimierte Prozesse bieten:

  • weniger Stress
  • wirkt professionell
  • fördert das miteinander
  • geben Freiraum für andere Gedanken (weitere Optimierungen)
  • helfen voraus zuschauen
  • geben einem ein sichereres Gefühl für die Tätigkeit (Selbstsicherheit)
  • Notfallprozeduren und Ausweichpläne
  • Freiräume

Ziel: Optimierung gut für das Arbeitsklima

Nach einiger Zeit in der Ausführung von optimierten Prozessen, werden diese beherrscht. Damit fällt der Stress weg, der Mitarbeiter wird sicherer und das spüren alle. Mitarbeiter, Chef und Kunden.
Optimierte Prozesse haben in der Regel auch kleinere Arbeitsschritte und kürzere Wege. Damit sinkt die Belastung enorm und der Mitarbeiter hat das Gefühl, die Arbeit ist leicht. 
Durch den geringeren Stress, steigt die Zufriedenheit. Man findet mehr Zeit auch an sich zu denken und ist nicht dauernd zu 100% eingespannt. 
Die Bedienung der Kunden erfolgt schneller und das freut vorallem eilige Kunden.
Die Mitarbeiterzufriedenheit wirkt sich auf den Kunden aus und dieser wird weniger mürrisch reagieren. Das löst eine Zufriedenheitsspirale aus, in der man sich gegenseitig aufbaut.

Ziel: Zufriedene Kunden

Zufriedene Kunden machen mehr Umsatz. Wichtiger noch:
Sie machen erneut Umsatz. Nur zufriedenen Kunden kaufen nochmal, akzeptieren etwas höhere Preise als die billige Konkurrenz und konsumieren Empfehlungen durch das Personal.
Besonders wichtig sind diese Aspekte wenn viel Konkurrenz in der Nähe ist.

Ziel: Mehr Umsatz

Oft fehlt dem einzelnen Mitarbeiter der direkte Zusammenhang zwischen verärgerter oder verlorenen Kunden und den Auswirkungen von Umsatzrückgang. Umsatzrückgang führt meist zu Einsparungen. Einsparungen führen meist zu Entlassungen.
Natürlich ist Umsatz nicht gleich Gewinn, aber ich gehe davon aus, dass diese Berechnungen schon optimiert sind und ein höherer Umsatz zu mehr Gewinn führt. Hier soll es nur um die Prozesse ansich gehen.

Vorgehen im Runden Gespräch

Zuerst wird der bestehende Ablauf untersucht. Anwendungsbeispiele zeigen auf, welche Probleme konkret gemeint sind. Danach werden Optimierungsmöglichkeiten angesprochen und durchgesprochen. Am Ende einigt man sich auf eine Versuchsreihe. Diese sollte auch unbedingt konsequent durchgeführt werden, damit die Optimierung nicht im Chaos und dann wieder ins alte System zurück fällt.

Unmotivierte Mitarbeiter identifizieren

Die Nörgler und diejenigen, welche nicht bereit sind, dass neue System zu akzeptieren müssen identifiziert werden. Diese sind sehr gefährlich für den Erfolg. Sie verbreiten schlechte Stimmung und können die Arbeitsmoral enorm sinken. Diese Mitarbeiter müssen besonders auf die neuen Prozesse abgestimmt werden. Sie brauchen etwas länger und darum sollten sich Manager und Arbeitgeber auch einstellen. Diese sollten auf verschiedene Ansätze zu überzeugten Mitarbeitern werden. Oft hilft hier eine Übertragung von Vorreiterrollen. Sie müssen sich verstanden und wichtig fühlen. Sie brauchen das Gefühl, dass man auf deren Meinung Wert legt und dass diese auch wahrgenommen wird. Am besten in Form von Akzeptanz und Umsetzung. 
Wenn man es sich leisten kann, ist es ok, auch mal einen Vorschlag von solchen Vorreitern umzusetzen, wenn dieser nicht optimal ist. Man kann dann, wenn alle erkannt haben, dass dieser Weg nicht so optimal war, wieder zurück schwenken. 
Wichtig ist hier, dass der Mitarbeiter gelernt hat, dass man auf Ihn hört. 
Viel wichtiger ist hier aber, dass man auf keinen Fall das Scheitern des Vorschlags ausweitet. Sondern hier sehr deutlich machen, dass nicht immer der optimale Weg probiert wird, aber nur durch das Testen dies auch erkannt werden kann. Dies fördert die Bereitschaft der Mitarbeiter Optimierungen überhaupt einzubringen.

Die Angst vor dem Versagen und damit der Blossstellung ist enorm gross. Man muss diese den Mitarbeitern nehmen.

Beispiel einer Optimierung an der Bestelltheke

Wir spielen ein Verkaufsszenario durch. Dabeit kommt zuerst die nicht optmierte Variante mit Wartezeiten und anschliessend eine leicht optimierte. Dann vergleichen wir die Zahlen. Zur Vereinfachung gehen wir davon aus, dass alles jeder Prozessschritt 1 Sek dauert. Das ist natürlich nicht sehr real, reicht aber um das Konzept zu veranschaulichen.
Verkaufstheke mit 4 Bereichen: Wurst, Kasse & Bestellannahme, Kaffee und Käse

Nicht optimierter Vorgang

  1. Kunde kommt an die Kasse
  2. Mitarbeiter fragt nach der Bestellung: (1 Sek)
    Was darf es sein?
  3. Kunde bestellt: (1 Sek)
    Ein Wurstbrot.
  4. Mitarbeiter geht zur Wurst-Theke (1 Sek)
  5. Mitarbeiter greift das Wurstbrot (1 Sek)
  6. Mitarbeiter kommt mit Wurstbrot zur Kasse (1 Sek)
  7. Verkaufsförderung: Mitarbeiter fragt nach weiteren Wünschen: (1 Sek)
    Darf es sonst noch etwas sein?
  8. Kunden überlegt kurz: (1 Sek)
    Noch einen Kaffee bitte.
  9. Mitarbeiter geht zur Kaffee-Theke  (1 Sek)
  10. Mitarbeiter startet Kaffeemaschine (1 Sek)
  11. Kaffee startet Mahlprozess (1 Sek)
  12. Kaffee startet Brühprozess (1 Sek)
  13. Kaffee läuft in den Becher (1 Sek)
  14. Mitarbeiter greift den Kaffeebecher (1 Sek)
  15. Mitarbeiter kommt mit Kaffee zur Kasse (1 Sek)
  16. Verkaufsförderung: Mitarbeiter fragt nach weiteren Wünschen: (1 Sek)
    Darf es sonst noch etwas sein?
  17. Kunden ist fertig (1 Sek)
    Nein Danke. Das war's
  18. Mitarbeiter berechnet Preis (1 Sek)
  19. Mitarbeiter nennt Preis (1 Sek)
    5 Euro bitte
  20. Kunde sucht Geld (1 Sek)
  21. Kunden bezahlt und geht mit den Waren weg (1 Sek)
Damit haben wir insgesamt 21 Schritte die 20 Sekunden benötigt. 1 Kunde, 1 Mitarbeiter. Alles läuft nach einander ab.

Optimierungsmöglichkeit

  1. Kunde kommt an die Kasse
  2. Mitarbeiter fragt nach der Bestellung: (1 Sek)
    Was darf es sein?
  3. Kunde bestellt: (1 Sek)
    Ein Wurstbrot.
  4. Verkaufsförderung: Mitarbeiter fragt nach weiteren Wünschen: (1 Sek)
    Sonst noch etwas?
  5. Kunden überlegt kurz: (1 Sek)
    Noch einen Kaffee bitte.
  6. Verkaufsförderung: Mitarbeiter fragt nach weiteren Wünschen: (1 Sek)
    Das wars?
  7. Kunden ist fertig (1 Sek)
    Ja, das ist alles
  8. Mitarbeiter berechnet Preis (1 Sek)
  9. Mitarbeiter nennt Preis (1 Sek)
    5 Euro bitte
  10. Kunde sucht Geld (0 Sek) - Paralleler Prozess, da Mitarbeiter nicht warten muss
  11. Mitarbeiter geht zur Kaffee-Theke  (1 Sek) 
  12. Mitarbeiter startet Kaffeemaschine (1 Sek)
  13. Kaffee startet Mahlprozess (0 Sek)  - Paralleler Prozess, da Mitarbeiter nicht warten muss
  14. Kaffee startet Brühprozess  (0 Sek)  - Paralleler Prozess, da Mitarbeiter nicht warten muss
  15. Kaffee läuft in den Becher  (0 Sek)  - Paralleler Prozess, da Mitarbeiter nicht warten muss
  16. Mitarbeiter geht zur Wurst-Theke (1 Sek)
  17. Mitarbeiter greift das Wurstbrot (1 Sek)
  18. Mitarbeiter geht zur Kaffee-Theke  (1 Sek) 
  19. Mitarbeiter greift den Kaffeebecher (1 Sek)
  20. Mitarbeiter kommt mit Wurstbrot und Kaffee zur Kasse (1 Sek)
  21. Kunde bezahlt und geht mit den Waren weg (1 Sek)
Damit haben wir insgesamt 21 Schritte die 16 Sekunden brauchen.
1 Kunde, 1 Mitarbeiter. Prozesse laufen zum Teil parallel.
Wir haben nur durch die Verlagerung der Fragen eine Information mehr gewonnen, die uns hilft einen Arbeitsprozess zu optimieren (Kaffee). Das frühe Nennen des zu zahlenden Preises, erspart uns auch das Warten auf den Kunden, bis er sein Geld abgezählt hat.
Wir haben einen Zeitersparnis von ca. 15%. Das ist enorm. 
Wenn wir dies hochrechnen, hat der Mitarbeiter bei gleichbleibender Kundenanzahl 15% mehr Zeit zum Entspannen. Die Arbeit wirkt gleich weniger stressig. Dieses gute Gefühl gibt der Mitarbeiter automatisch weiter. Man liest es in seinem Gesicht. 

Fazit

Dies war nur ein sehr kleiner Ausflug in die Prozessoptimierung. Jedoch ist schon sehr deutlich geworden, was man da alles noch machen kann. Wichtig ist aber immer das Einbeziehen des Mitarbeiters. Oft sind schlechte Mitarbeiter die Schuld des Vorgesetzten oder der Firmenpolitik. 

Freitag, 14. Dezember 2012

No nodejs für arbeitsintensive Dauerleistung


Die Wunderwaffe für die Webentwicklung ist nicht als Arbeitstier-Skriptwerkzeug zu gebrauchen. Solange Events ankommen und Daten aus dem Arbeitsspeicher gesendet werden sollen, ist nodejs wirklich effizient. Aber stupides Datenauslesen, Dateien anlegen und diese wieder verarbeiten bringt nodejs schnell aus der Puste.

Klar es läuft nur auf einem Thread und das spürt man dann auch. Es arbeitet ineffizient bei vielen Datenzugriffen.

Durch die asynchronen Abläufe kommt es leider auch zu ungewünschten Nebeneffekten. Das Abarbeiten von grossen Aufträgen kann damit zur Tortur mit OutOfMemory werden.

Der Versuch ist einfach: 


- Lese 10.000 Datensätze aus der Datenbank
- Speicher die Daten in Dateien
- Öffne diese Dateien und lese Inhalt ein
- Versende Inhalt per Webrequest
- Erfasse Server-Antwort in Datei
- Benenne Datei mit Zusatz "closed" um

Was machen wir, um dieses Verhalten zu testen?

Dazu ein einfaches Skript,welches Daten aus einer Datenbank zieht. Ob wir nun alle Datensätze abholen und dann über diese iterieren, oder bei jedem Zeilentreffer die Verarbeitung starten ist egal. Diese minimalen Unterschiede können wir ignorieren.

Wir brauchen noch eine Funktion, die den Dateninhalt einer Zeile als JSON abspeichert. Zuerst wählen wir die asynchrone Variante. Die Funktion erstellt eine Datei mit Namen: Datensatz-Id_OPEN.txt. Am Ende dieser Funktionen rufen wir das Abarbeiten der Datei auf. 

Dazu erstellen wir eine weitere Funktion, welche die Datei öffnet, einliest und einen Webserver kontaktiert. Wir senden die Daten als POST-Request. Geht auch sehr einfach mit nodejs. Die Antwort vom Server hängen wir an die Datei und benennen diese wiederum um. Neuer Name: Datensatz-Id_CLOSED.txt.

Warum diese Aufwand? 

Dadurch, dass die schreibende Funktion nicht auch gleich versendet, können wir eine Prozess-Wiederaufnahme Option einbauen. So könnten wir eine separate Funktion starten, welche über die Dateien geht, welche nicht im Zustand (im Namen) CLOSED haben und diese dann verarbeiten. Im Grunde wäre es hier ratsam, noch einen weiteren Status einzuführen: PROCCESSING oder so, damit man auch erkennt, welche direkt in der Mache waren. Können wir uns sparen und für diesen Test nehmen wir an, dass der Server Duplikate verwirft.

Was passiert nach dem Starten vom Skript?

Wir starten das nodejs Skript (ohne Aufräumarbeit durch separeten Aufruf für abgebrochene Übertragungen) und zuerst werden die Daten aus der Datenbank geholt. Das geht sehr schnell, so schnell wie es die DB erlaubt. Dann iterieren wir über die Datensätze und rufen die Funktion auf, zum erstellen der Dateien.

Hier das erste interessante. Damit wir mitbekommen, was passiert geben wir bei jedem Funktionsaufruf eine Message auf der Konsole aus. Wir sehen, dass nun 10.000 mal die Funktionen für das Datei erstellen aufgerufen wird. Aber die Message für das schreiben in die Datei erscheint nicht. Ein Blick in den Ordner zeigt auch, dass tatsächlich keine Datei erstellt wird. Das Skript baut die JSON-Texte auf und hat vor dem  Schreiben scheinbar einfach alles liegen lassen. Nach den 10.000 Meldungen, schreibt er nun tatsächlich. Er beginnt die Dateien anzulegen. Nach dem Anlegen sollte die Verarbeitung starten, doch das passiert nicht. 

Ja klar, ist alles asynchron, jedoch sollte man meinen, dass er nicht erst alle Dateien anlegt, bevor er dann zum Verarbeiten (Senden der Daten an den Webserver) kommt. Doch genau das passiert. Bis hierher kann man noch mehr oder weniger das Verhalten akzeptieren.

Um sicher zu gehen, dass auch nicht unsere Vorgehensweise Schuld ist, ändern wir die Datenbankabfragen-Funktion so, dass nun jede Zeile verarbeitet wird. Doch genau das Gleiche Phänomen. Um nun mehr zu forcieren, wird auch das Schreiben nun synchron gemacht. Das verbessert ganz leicht den Ablauf, aber zu Lasten von Geschwindigkeit. 

Irgendwann beginnt nun dann die Verarbeitung des Sendens. Das Skript beginnt mit dem Lesen und meldet dies auch pflichtbewusst. 10.000 Dateien werden nun geöffnet, ohne dass scheinbar gelesen wird!!! Selbst wenn er die Daten gelesen hat, werden die weiteren Befehlszeilen nicht ausgeführt. Der Speicher läuft, das ganze System bekommt langsam Probleme. 

Erst nachdem scheinbar alle 10.000 Dateien gelesen wurden, beginnt er mit dem Versenden. Doch die Antwort wird nicht erfasst. Stattdessen müssen wir wiederum warten bis alle 10.000 Webrequests versendet sind. Selbst wenn wir das einlesen synchron machen, haben wir spätestens beim "immer" asynchron arbeitenden Web-Request die Probleme. 

Fazit

Nodejs ist super, wenn man einen eigenen flinken Webserver braucht, um einen Dienst bereit zu stellen, der wenig auf der Festplatte arbeitet. Alle File-Operationen sind grausam. Leider ist damit nodejs für mich nicht mehr in der engen Wahl, wenn es darum geht, grosse Datenmengen zu verarbeiten. Das ist nicht sein Metier und man sollte es auch nicht erzwingen. 

Mittwoch, 21. November 2012

Einfacher Webclient für POST und GET Formulare Senden Tests

Webclient für einfache POST und GET Requests

Wer kennt das nicht. Man will eben ein paar Abfragen an das neue Serverprogramm oder an die neue PHP-App senden und muss dabei die Parameter immer wieder anpassen. Mühselig, kann man die URL entsprechend in den Browser tippen, doch dann muss man plötzlich POST senden.

Damit man nicht immer wieder ein neues HTML-Formular oder ein spezielles PHP-Skript oder gar per Console eine Curl Aktion draus macht, wäre ein komfortables Webseiten-Tool doch ganz nett. Am besten gleich mit History, um einen Request nochmal senden zu können.

Voila:

 <html>  
 <head><title>Testclient für Webaufrufe</title>  
 <meta charset="utf-8">  
 <script type="text/javascript">  
 function makeURIParameterForm(text, form) {  
      // entferne alt lasten  
      // gehe über alle elemente  
      // wenn eines dyn gesetzt wurde entfernen, bevor die neuen kommem  
      var elemente = []; // werden entfernt
      for (var a=0;a<form.elements.length;a++){  
           var elem = form.elements[a];  
           if (elem.tag && elem.tag == "dyn"){  
                elemente.push(elem);  
           }  
      } 
      for (var a=0;a<elemente.lenght;a++)
           form.removeChild(elemente[a]);

      // extrahiere die elemente und  
      var teile = text.split("&");  
      for (var a=0;a<teile.length;a++){  
           var t = teile[a].split("=");  
           var elem = document.createElement("input");  
           elem.type = "hidden";  
           elem.value = t[1];  
           elem.name = t[0];  
           elem.tag = "dyn";   
           form.appendChild(elem);  
      }   
 }  
 function senden(form){  
           var object = {};  
           object.post = form.elements[1].checked;  
           object.target = form.elements[2].value.trim();  
           object.url = form.elements[0].value.trim();  
           object.body = form.elements[3].value.trim();  
           object.form = form;    
           liste.push(object);  
           var elem = document.createElement("option");  
           elem.value = liste.length - 1;  
           elem.text = "["+((object.post)?"POST":"GET")+"] "+object.body;  
           document.getElementById("liste").appendChild(elem);  
           fuelleForm(form, object);             
 }  
 function ladeForm(idx){  
      var object = liste[idx];  
      fuelleForm(object.form, object);  
      document.getElementById("info").innerHTML = "Form geladen: idx: "+idx;  
 }  
 function zeigeForm(idx) {  
      var object = liste[idx];  
      var text = "IDX: "+idx+"<br>";  
      for (var key in object)  
           if (key != "form")  
                text += '<b>'+key+"</b>: "+object[key]+"<br>";  
      document.getElementById("info").innerHTML = text;       
 }  
 function fuelleForm(form, object) {  
      form.target = object.target;  
      form.elements[3].value = object.body;  
      var url = object.url;  
      var body = object.body;
      if (url.indexOf("?")>0){
           var pos = url.indexOf("?");
           body = url.substr(pos+1)+"&"+body;
      }
      makeURIParameterForm(body, form);       
      if (object.post){  
           form.elements[1].checked = true;  
           form.method='POST';  
      } else {  
           form.elements[1].checked = false;  
           form.method='GET';  
      }  
      form.action = url;  
      document.getElementById("info").innerHTML = "Gesendet an: "+form.action;  
 }  
 var liste = [];  
 </script>  
 </head>  
 <body>  
 <div style="width:70%;float:left;">  
 Daten senden
 <form onsubmit="senden(this);return true;">  
 url: <input type=text style="width:70%;" hint="http://localhost/index.php"><br>  
 <input type=checkbox> POST<br>  
 target: <input type=text value="ziel"><br>  
 Body (bsp: para1=1213&amp;para2=2233):<br>  
 <textarea style="width:100%;"></textarea><br>  
 <div style="text-align:right;"><input type=submit></div>  
 </form>  
 </div>  
 <div style="width:25%;float:right;">  
 <form onsubmit="ladeForm(this.elements[0].options[this.elements[0].options.selectedIndex].value);return false;">  
      <select size=10 style="width:100%;" id="liste" onclick="zeigeForm(this.options[this.options.selectedIndex].value);">  
      </select><br>  
      <div style="text-align:right;"><input type=submit value="laden"></div>  
 </form>  
 </div>  
 <div style="clear:both;"></div>  
 <hr>  
 <div id="info"></div>  
 <hr>  
 <iframe name="ziel" style="width:100%;heigth:300px;boder:1px solid blue;"></iframe>  
 </body>  
 </html>  

Warum nur 300px für das Iframe? Weil ich den Debugger noch unten eingeblendet habe. Also nur ein Platz Problem. Könnt Ihr euch ja beliebig anpassen.

Viel Spass damit
Saso Nikolov

Freitag, 9. November 2012

Websockets scheinen tot, bevor diese richtig die Welt erblickt haben

Websockets Ade

Wer schon mal versucht hat Websockets einzusetzen, hat bestimmt schon erlebt, wie unnötig kompliziert alles ist. Hat man die Clientseite gemeistert, die zugegebener Massen einfach ist. Wird die Serverseite zum Horrorgang. Wohl gemerkt, immer mit der Prämisse, dass man alles selbst programmiert und keine Software anderer einbindet.

Wozu Websockets?

Man kann Websockets dazu nutzen, dass man im Webbrowser über Aktionen sofort informiert wird. Der gern genommen Chatserver soll hier als Beispiel dienen. Wäre doch cool, ohne Flash und anderem einfach ein Chat in HTML anzubieten. Würde dann auch auf allen mobilen Geräten funktionieren und im Browser deiner Wahl. 

Allerdings gibt es keine direkte Verbindung vom Server zurück zum Browser, wenn die Seite ausgeliefert wurde. Wenn nun einer im Chat was schreibt, müssten die anderen Chatter immer wieder nach Aktualisierungen beim Server fragen. Das ganze muss dann noch synchronisiert werden, damit die Antworten passen in der Reihenfolge angezeigt werden. Wenn das Intervall der Abfragen an den Server für Änderungen niedrig ist, dann kommt alles sehr verzögert an. Sind die Intervalle zu hoch, legen wir Browser und Server lahm.

Das alles wird mit den Websockets behoben. Der Browser baut eine Verbindung zum Server auf und hält diese offen. Sobald eine Aktualisierung im Server bekannt wird (ein Chatter hat was gesagt), sendet der Server diese Nachricht an alle offenen Websockets und der Browser schreibt mittels Javascript die Nachricht sofort auf die Webseite.

Das Ganze ist auch Ideal um Spiele und anderes Event bezogenen Überwachungen im Webbrowser zu realisieren. Kein Installieren und dennoch up2date.

Toll, doch was ist nicht gut?

Nicht gut ist, dass Websockets noch nicht von allen Browser unterstützt wird und noch weniger der Otto-Normal-Entwickler dies auf seinem angemieteten Server zusammen programmieren kann.

Wir brauchen eine Lösung, die mit jedem Browser (egal wie alt funktioniert). Allerdings benötigen wir mindestens Javascript bei allen Lösungen.

Alternativen

Ok, was können wir machen. Das übliche Überlegen. Nicht immer sofort das neueste implementieren, sondern sehen, ob wir es dem Kunden nicht so einfach wie möglich machen wollen.

Nach einer kurzen Analyse des Browser sehen wir einen Ansatzpunkt. Der Browser erstellt per Javascript ein Script-Tag im Header. Dieser dynamisch nachgeladene Javascript Code realisiert die Aktualisierungen. Das habe ich im Apicall-Verfahren schon demonstriert. Doch dann muss das Intervall hochgemacht werden?
Nein. Wir erinnern uns an damals, als das Internet noch sehr langsam war. Damals wurde einfach gewartet. Das tun wir in unserer Lösung auch. 

Die meisten Browser brechen nach 1 Minute Wartezeit ab. Also setzen wir hier an und wählen 30-40 Sekunden. Der Browser verbindet sich mit dem Server. Der Server antwortet nicht sofort. Sondern speichert diese Verbindung global. Kommt nun eine Aktualisierungsmeldung. Werden alle gehaltenen (verzögerten) Javascript -Code Wünsche durchlaufen und jeder bekommt die Aktualisierung gemeldet.

Der Browser baut daraufhin sofort wieder eine Verbindung mittels Apicall auf. Auf dem Server lassen wir einen Job laufen, der immer wieder die veralteten Verbindungen beantwortet. Einfach eine leere Meldung in die Apicall Antwort einbauen. Dann ist alles sauber. So kann man auch komplett abgemeldete Verbindungen killen. 

Das Ganze ist nicht so einfach mit PHP zu lösen. Doch es geht. Hier müsste man einen gemeinsamen Speicher nutzen. Meine Lösung ist in node.js realiesert. Ich bin einfach begeistert, was man mit nodejs machen kann. Schnell und einfach kann man wunderbare Sachen zaubern. Seht euch das Video auf der Seite an!

Code Beispiel für eine Chat-App:

HTML (Der Client):

Die Basics.js ist ein Sammlung von Hilfsfunktionen von Directpaylink:
http://directpaylink.com/javascript/basics.js
Da ist auch viel unnötiges drin, so dass ich immer eine Kopie herunter lade und diese dann von Ballast befreie.


 <html>  
 <head>  
 <meta charset="utf-8">  
 <style type="text/css">  
 li {list-style-type:none;padding-left:2px;}  
 .msg:nth-of-type(even) { background: #dedede; }  
 .msg:nth-of-type(odd) { background: #fefefe; }  
 </style>  
 <script type="text/javascript" src="basics.js"></script>  
 <script type="text/javascript">  
 var system = {};  
 system.url = "http://localhost:8080/";  
 system.nohtml = true;  
 function konnektieren(adresse, usr) {  
      system.url = "http://"+adresse+":8080/?usr="+encodeURIComponent(usr);  
      system.usr = usr;  
      system.adresse = adresse;  
      message("Verbinde mit: "+system.url);  
      verbinden(true);  
 }  
 function verbinden(force) {  
      message("Hole Sessionid");  
      // wenn kein session code vorhanden  
      if (!force && system.sessionid)  
           return true;  
      var url = system.url;       
      apicall(url,   
           function(h) {  
                // sessionid als antwort  
                var t = h.responseText.split(";");  
                system.sessionid = t[0];  
                message("sessionid bekommen: "+system.sessionid);  
                document.getElementById("chat").style.display = "block";  
                message("gehe über auf Events zu warten. Chatten ist möglich");  
                system.wartesekunden = t[1];  
                setInhalt("info", system.wartesekunden);  
                warteAufEventsVomServer(system.wartesekunden);  
                document.getElementById("tt").select();  
           },   
           function(h){  
                message("fehler sessionid abzuholen. Kein weiterer versuch");                 
           });       
 }  
 function warteAufEventsVomServer(wartesekunden) {  
      if (!wartesekunden)  
           wartesekunden = 20*1000;  
      if (!system.sessionid)  
           return alert("keine session vorhanden");  
      var url = system.url+"&sessionid="+system.sessionid;       
      // baue verbindung zum server auf  
      apicall(url,   
           function(h) {  
                if (checkResponse(h.responseText)) {  
                     //message("chattext erhalten [EVENT]: "+h.responseText);  
                     var code = h.responseText.substr(0,h.responseText.indexOf(":")).trim().toLowerCase();                      
                     switch(code) {  
                          case "reload":  
                               reload();  
                          case "chat":  
                               if (system.nohtml) {  
                                    message('<b>'+Date2Text()+" "+removeHTMLTags(decodeURIComponent(h.responseText.substr(6)))+'</b>');  
                               } else {  
                                    message('<b>'+Date2Text()+"</b> "+decodeURIComponent(h.responseText.substr(6)));  
                               }  
                               // bei antwort, erneut verbinden  
                               warteAufEventsVomServer();  
                     }  
                } else { // error  
                     system.sessionid = false;  
                     verbinden(); // neue session aufbauen       
                }  
           },   
           function(h){  
                //message("kein event. warte auf den nexten event");                 
                // bei timeout erneut verbinden  
                warteAufEventsVomServer(wartesekunden);  
           }, wartesekunden);       
 }  
 function reload() {  
      var t = "http://"+window.location.host+window.location.pathname+"?reload&usr="+  
           encodeURIComponent(system.usr)+"&sessionid="+  
           encodeURIComponent(system.sessionid)+"&adresse="+  
           encodeURIComponent(system.adresse)+"&wartesekunden="+  
           encodeURIComponent(system.wartesekunden);  
      document.location = t;  
 }  
 function usrlist() {  
      if (!system.sessionid)  
           return alert("keine session vorhanden");  
      var url = system.url+"&sessionid="+system.sessionid+"&cmd=usrlist";       
      apicall(url,   
           function(h) {  
                message(Date2Text()+" User-Liste:");  
                var liste = h.object;  
                for (var a=0;a<liste.length;a++)                                     
                     message(Date2Text()+" " + liste[a]);                 
           });  
 }  
 function senden(msg, elem) {  
      // sende den Text  
      if (!system.sessionid)  
           return alert("keine session vorhanden");       
      elem.select();  
      var url = system.url+"&sessionid="+system.sessionid+"&chat="+encodeURIComponent(system.usr+": "+msg);       
      apicall(url,   
           function(h) {                 
                message(Date2Text()+": " + msg);                 
           },   
           function(h){  
                message("chattext konnte nicht gesendet werden");                 
           });       
 }  
 function setNOHTML(wert) {  
      system.nohtml = wert;  
 }  
 function message(text) {  
      var elem = document.getElementById("inhalt");   
      elem.innerHTML += '<li class="msg">'+text+'</li>';  
      document.getElementById("container").scrollTop = elem.offsetHeight;  
 }  
 function starten() {  
      system.REQUEST = basics_ermittelURLParameter();  
      if (system.REQUEST['reload']) {            
 //          system.nohtml = system.REQUEST['nohtml'];  
           system.usr = system.REQUEST['usr'];  
           system.sessionid = system.REQUEST['sessionid'];  
           system.adresse = system.REQUEST['adresse'];            
           system.wartesekunden = system.REQUEST['wartesekunden'];  
           system.url = "http://"+system.adresse+":8080/?usr="+encodeURIComponent(system.usr);  
           document.getElementById("adr").value = system.adresse;  
           document.getElementById("usr").value = system.usr;  
           //warteAufEventsVomServer(system.wartesekunden);  
      }  
 }  
 window.onload=starten;   
 </script>  
 </head>  
 <body>  
 <form onsubmit="konnektieren(this.elements['adr'].value, this.elements['usr'].value);return false;">  
 chat.server: <input type=text id="adr" name="adr" value=""> <br>  
 username: <input id="usr" name="usr" type=text> <input type=submit value="connect">  
 </form>  
 <div id="chat" style="display:none;">  
      <input type=checkbox id="cnohtml" onchange="setNOHTML(this.checked)" checked> Keine HTML ausführen  
      <form onsubmit="this.elements['btn'].click();return false;">  
      <input type=text id="tt" name="tt"><button name="btn" onclick="senden(document.getElementById('tt').value, document.getElementById('tt'));return false;">senden</button>  
      </form>  
      <div id="container" style="height:300px;overflow-y:scroll;">  
           <div id="inhalt"></div>  
      </div>  
      <button onclick="reload()">reload</button>  
      <button onclick="usrlist()">usrlist</button>  
      <span id="info"></span>  
 </div>  
 <body>  
 </html>  

Javascript für nodejs (Der Server)


 var HTTP = require("http");   
 starten(); // damit kann man testzwecke besser steuern  
 function starten() {  
      webserver();   
 }  
 function webserver(meinserverport) {  
      if (!meinserverport)  
           meinserverport= 8080;  
      console.log("starte server");  
      function vb_clean() {  
           console.log("entferne abgelaufene sessions");  
           var ablauf = time() - (60*1000); // 1 minuten  
           for (var key in vb){  
                if (vb[key].lastconnect < ablauf) {  
                     console.log("entferne", vb[key]['usr'], vb[key].sessionid);  
                     var msg = vb[key].usr+" ist offline";  
                     delete(vb[key]);  
                     schreibeAllen("", msg);  
                }   
           }  
           setTimeout(vb_clean, 30*60*1000);  
      }  
      function schreibeAllen(sessionid, msg) {  
           if (!sessionid)  
                sessionid = "";  
           for (var key in vb) {  
                if (!vb[key].response)  
                     continue;  
                if (key == sessionid)  
                     continue;   
                var html = vb[key].callbackdaten['cbf']+"('"+vb[key].callbackdaten['cbid']+"',"+JSON.stringify("Chat: "+msg)+");";  
                vb[key].response.writeHead(200, {"Content-Type":"text/javascript"});  
                vb[key].response.write(html);  
                vb[key].response.end();  
                vb[key].lastconnect = time();  
           }  
      }       
      vb_clean();  
      var wartezeit = {'client':40*1000, 'server':45*1000};  
      var s = HTTP.createServer(function(request, response) {  
           console.log(request.connection.address());                 
             console.log("Verbindungsaufbau: "+request.connection.remoteAddress, request.connection.remotePort);  
                var url = require('url');  
                var rq = url.parse(request.url, true);  
                //console.log(rq);  
                  console.log(request.method, rq.pathname+rq.search);  
                var paras = rq.query;  
                // der weg über a kann später entfallen  
                // denkbar auch der Weg über die pfad angabe - url.parse(req.url).pathname  
                if (paras['js']) {  
                     // sende das JS  
                     // header('Content-Type: text/event-stream');                       
                     response.writeHead(200, {  
                          "Content-Type":"text/javascript"                           
                          }); // verbindung ok            
                     console.log("Starte Live-JS Eingaben:");  
                     // machen wir über die tastatur, die ruft cbfkt auf für die Übermittlung  
                     nehmeTastatur(function(t){  
                          response.writeHead(200, {"Content-Type":"text/javascript", "Content-Length":t.length});  
                          response.write(t);                           
                          }, function(){  
                               response.end();  
                     });  
                } else if (paras['cbf']) {  
                     // apicall kommt rein  
                     if (paras['sessionid']) {  
                          if (paras['chat']) {  
                               // msg ist eingetroffen  
                               // sender ok senden  
                               var html = paras['cbf']+"('"+paras['cbid']+"','OK;"+wartezeit.client+"');";  
                               response.writeHead(200, {"Content-Type":"text/javascript"});  
                               response.write(html);   
                               response.end();  
                               // verteile an alle erstmal  
                               schreibeAllen(paras['sessionid'], paras['chat']);  
                          } else {   
                               if (paras['cmd']) {  
                                    console.log("usrliste senden");  
                                    response.writeHead(200, {"Content-Type":"text/javascript"});  
                                    var html = "";  
                                    switch (paras['cmd']) {  
                                         case "usrlist":  
                                              var liste = [];  
                                              for (var key in vb) {  
                                                   if (!vb[key].response)  
                                                        continue;  
                                                   //if (key == paras['sessionid'])  
                                                   //     continue;  
                                                   liste.push(vb[key].usr);  
                                              }                                               
                                              html = paras['cbf']+"('"+paras['cbid']+"',"+JSON.stringify(liste)+");";  
                                              break;  
                                    }  
                                    response.write(html);   
                                    response.end();  
                               } else {                                  
                                    // wartet auf events  
                                    if (!vb[paras['sessionid']]) {  
                                         console.log("sessionid nicht bekannt: "+paras['sessionid']);  
                                         var html = paras['cbf']+"('"+paras['cbid']+"','ERROR: session nicht vorhanden');";  
                                         response.writeHead(200, {"Content-Type":"text/javascript"});  
                                         response.write(html);   
                                         response.end();  
                                         return;  
                                    }  
                                    vb[paras['sessionid']].response = response; // der browser wird nach 10sek abbrechen und sich erneut verbinden  
                                    vb[paras['sessionid']].lastconnect = time();  
                                    vb[paras['sessionid']].callbackdaten = {cbf:paras['cbf'],cbid:paras['cbid']};  
                                    setTimeout(function(){  
                                         console.log("schliesse veraltet Connection");  
                                         if (response) {  
                                              response.writeHead(200, {"Content-Type":"text/javascript"});  
                                              response.end();  
                                         }       
                                    }, wartezeit.server); // der client baut die verbindung für 20 Sek auf  
                               }  
                          }   
                     } else {  
                          if (!paras['usr'])  
                               paras['usr'] = "guest_"+request.connection.remoteAddress+'_'+request.connection.remotePort;  
                          // session erstellen und zurück geben  
                          response.writeHead(200, {"Content-Type":"text/javascript"});  
                          var sessionID = require("crypto").createHash("sha224").update("" + new Date().getTime() + Math.random() + '.sasoleindenys').digest("hex");  
                          vb[sessionID] = {  
                               'zeit': time(),  
                               'lastconnect': time(),  
                               'response':null,                                
                               'usr': paras['usr']  
                               };  
                          var html = "";  
                          html += paras['cbf']+"('"+paras['cbid']+"','"+sessionID+";"+wartezeit.client+"');";  
                          response.write(html);   
                          response.end();  
                     }                  
                } else {  
                     // normale HTTP verbindung  
                     // sende das HTML  
                     var html = "";  
                     if (rq.pathname.match(/chatapp/)) {  
                          console.log("sende die chatapp HTML datei");  
                          response.writeHead(200, {"Content-Type":"text/html"}); // verbindung ok  
                          html = require("fs").readFileSync('apicall_chat.html', "binary");  
                     } else if (rq.pathname.match(/basics\.js/)) { // javascript hilfsfunktionen  
                          console.log("sende die chatapp HTML datei");  
                          response.writeHead(200, {"Content-Type":"text/javascript"}); // verbindung ok  
                          html = require("fs").readFileSync('basics.js', "binary");  
                     } else {  
                          response.writeHead(200, {"Content-Type":"text/html"}); // verbindung ok  
                          html = '<html><head><script src="?js=1" type="text/javascript"></script></head><body>Versuch es mal mit /chatapp</body></html>';  
                     }  
                     response.write(html, "binary");            
                     response.end();  
                }  
                //response.writeHead(404, ""); // du bist hier falsch  
                //response.end();  
      }).listen(meinserverport);  
      function time(onlyseconds) {  
           var datum = new Date();  
           var milliseconds = datum.getTime();  
           if (onlyseconds)  
                return intval(milliseconds/1000);  
           return milliseconds;  
      }  
      var vb = {}; // webchat client  
 }  

Fazit

Es geht auch ohne Websockets. Gerade wenn man auf Nummer sicher gehen will und auch den Server entsprechen nutzen kann. Mein Versuch hilft euch hoffentlich für viel bessere Lösungen als hier zusammengestellt.

Viel Spass
Saso Nikolov

Dienstag, 9. Oktober 2012

Fortschritt hält die Evolution auf

Änderungen an uns, in unserer Evolution, war immer durch eine Unzulänglichkeit begleitet.
Unsere Art hat sich immer an etwas angepasst und damit haben wir uns weiter entwickelt.

Doch mit der ganzen Technologie, die uns alles abnimmt, welche uns vor bestimmten Ereignissen und Gegebenheiten schützt, werden wir uns eher zurück entwickeln, als nach vorne.

Warum sollte der Körper sich an etwas anpassen, wenn wir uns die Bedingungen anpassen. Wir helfen unserem Körper immer mehr nach, so dass er nicht die Notwendigkeit verspürt sich selbst in diese Richtung zu entwickeln.

Klar, dass nun nicht jeder seine Medikamente und Vitaminpillen entsorgen sollte, jedoch wäre es durchaus ratsam hier sparsamer zu sein.

Mal schnell umgeschaut: Immer mehr Menschen sind mit Brille ausgestattet. Immer mehr schwächeln vor sich hin. Vielleicht sollten wir uns mal selbst hinterfragen. Die ganzen Hilfsmittel werden nicht dazu beitragen, dass wir uns selbst verbessern.

gruss saso