Mittwoch, 29. Mai 2013

IRC-Bot mit NodeJS

Viele Admins und auch andere sind immernoch im IRC unterwegs. Schön wäre es doch, wenn ein Bot einem die Arbeit abnimmt, verschiedene Webrequests vornimmt und dann das Ergebnis in einen Channel postet. So kann man frei von E-Mails sich nach eigenen Vorlieben über bestimmte Zustände informieren lassen.

Was wird erstellt?

Ein IRC-Client, der auch bestimmte Befehle reagieren kann. Dieser ruft eine Webseite auf und liefert das Ergebnis als Chatnachricht zurück.

Erkenntnisse

Funktionsweise vom IRC. Einblicke in NodeJS als Server-Client-Gespann.

Vorarbeiten

Der Client verbindet sich über eine gesicherte Verbindung (SSL) mittels TLS.

Wir brauchen:
  • NodeJS (installiert oder auch nur die ausführbare Datei)
    http://nodejs.org/download/
  • IRC Account auf einem Server
  • Webseite, die einem was interessantes liefern

Konstrukt

Hier sind einige Elemente verbaut.
Mehrer Hilfsfunktionen und viele anonyme Funktionen.
Das Konstrukt soll auch zeigen, was alles möglich mit Javascript ist.

Zuerst generieren wir uns ein Objekt mit den Server-Zugangsdaten.
Der ganze Server ist einer Funktion eingebettet.

Die Hilfsbereiche sind zum Teil direkt in der Daten-Funktion eingebunden und zum Teil in einem Hilfsobjekt ausgelagert. Das soll zeigen, dass mehrer Methoden möglich sind.

Ablauf

  • Verbindung zum Server
  • Login
  • Registrieren von Event-Handler für den eingehenden Datenverkehr
  • Auswerten der eingehenden Texte
    • Aufrufen von entsprechenden Funktionen
    • Antworten in den Chat senden

Code

 var config = {  
      user: {  
           nick: 'sasobotli',  
           user: 'Saso_T_2013',  
           real: 'saso Bot - SN2013',  
           pass: '20cC12#'  
      },  
      server: {  
           addr: 'testirc.MEINSERVERNAME.de',  
           port: 6697  
      },  
      chans: ['#team-chat']  
      //chans: ['#chan1','#chan2']   
      , keywords:{}  
 }  
 // http://www.ietf.org/rfc/rfc1459.txt  
 irc3(); // startet den IRC-Bot  
 function irc3() {  
      // SSL Verbindung  
      var tls = require('tls');   
      var cleartextStream = tls.connect(  
                config.server.port,   
                config.server.addr,   
                null,  
                // anonyme Funktion, die das Anmelden auf dem Server macht   
                function(){  
                     irc.stream = cleartextStream;  
                     // ein paar Log-Ausgaben, um mehr zu verstehen   
                     console.log("client connected mit "+  
                          cleartextStream.remoteAddress+":"+  
                          cleartextStream.remotePort);  
                     console.log("meine adresse: ",   
                          cleartextStream.address());  
                     console.log('client connected',   
                          cleartextStream.authorized ? 'authorized' : 'unauthorized');  
                     // nun senden wir unsere Zugangsdaten  
                     irc.senden('PASS '+config.user.pass);  
                     irc.senden('NICK '+config.user.nick);  
                     irc.senden('USER '+config.user.user+  
                          ' localhost '+config.server.addr+  
                          ' '+config.user.real);  
                     // in alle channels anmelden  
                     for (var a=0;a<config.chans.length;a++) {       
                          irc.senden('JOIN '+config.chans[a]);  
                     }            
                     // kommandozeile einbinden,   
                     // so können wir auch direkt mit dem Server sprechen :)   
                     process.stdin.setEncoding('utf8');  
                     process.stdin.pipe(cleartextStream);  
                     process.stdin.resume();  
                }  
           );  
      // das Encoding setzen  
      cleartextStream.setEncoding('utf8');  
      // nun registrieren wir eine Funktion,  
      // die auf eingehende Daten reagieren soll  
      cleartextStream.on('data', function(data){  
           console.log("incoming");  
           // daten zeilenweise abarbeiten  
           data = data.split('\n');  
           for (var i = 0; i < data.length; i++) {  
                // leere zeilen überspringen  
                if (data[i].trim() == "")  
                     continue;  
                console.log('RECV -', data[i]);  
                if (data !== '') {  
                     // anforderungen / request vom Server sich zu melden  
                     var pos = data[i].indexOf(":");  
                     var key = data[i].substr(0,pos).trim().toUpperCase();  
                     var value = data[i].substr(pos+1).trim();  
                     if (key != "") {  
                          switch (key) {                      
                               case "PING":  
                                    irc.senden('PONG :'+value);       
                                    break;  
                               default:   
                                    console.log("Nicht abgefangen: "+key);  
                          }  
                     } else {  
                          // antwort oder info vom Server  
                          var teile = value.split(" ");  
                          //console.log(teile);  
                          switch(teile[1]) {  
                               case '353': // name reply, für NAMES,   
                                    // also alle Chatter in einem Raum erhalten  
                                    //console.log("Namen eingetroffen");  
                                    var namen = value.split(":")[1].trim().split(" ");  
                                    //console.log("gefunden: "+namen.length, namen);  
                                    for (var a=0;a<namen.length;a++){  
                                         var oname = namen[a].replace(/@/,"");  
                                         var admin = false;  
                                         if (namen[a].match(/@/))  
                                              admin = true;  
                                         // irc ist unten als Objekt definiert  
                                         irc.users[oname] = {'name':oname, 'admin':admin, 'nachrichten':[]};  
                                    }  
                                    //console.log(irc.users);  
                                    break;  
                               case 'JOIN': // Raum beigetreten  
                                    var raum = teile[2].substr(1);  
                                    // ein paar Begrüssungen  
                                    var sp = ['Hallo', 'Hey', 'hi', 'Wie gehts?',   
                                         'huhu','Toll, dass du da bist'];  
                                    // eine Begrüssung per Zufall auswählen  
                                    var pos = Math.round(Math.random()*(sp.length-1));  
                                    // begrüssen  
                                    irc.senden("PRIVMSG "+raum+" "+sp[pos]);  
                                    // alle Namen im Raum abrufen  
                                    irc.senden("NAMES "+raum);  
                                    break;  
                               case 'PRIVMSG':  
                                    // nachricht eingetroffen  
                                    // wir speicher wann zuletzt was gesagt wurde   
                                    irc.zaehler.lastword = time();  
                                    // ermittel ob direkt oder raum chat                                     
                                    var sender = teile[0];  
                                    var empfaenger = teile[2];  
                                    var user_sender = sender.split("!")[0];  
                                    var antwort = teile.slice(3)  
                                              .join(" ").trim()  
                                              .substr(1); // ohne führendes :  
                                    // wir speichern den User in unsere Liste  
                                    if (!irc.users[user_sender]) {   
                                         irc.users[user_sender] = {  
                                              'name':user_sender,   
                                              'admin':false,   
                                              'nachrichten':[], keywords:{}};  
                                    }  
                                    // speichern die Nachricht in seine History  
                                    irc.users[user_sender].nachrichten.push(antwort);  
                                    //console.log("nachricht eingegangen. Von "+sender, antwort);  
                                    // wurden wir direkt angesproche? also privatchat?  
                                    if (empfaenger.toLowerCase() ==   
                                              config.user.nick.toLowerCase()) {  
                                         var meineantwort = "danke, selber";  
                                         // senden unsere Antwort  
                                         irc.senden('PRIVMSG '+user_sender+" "+meineantwort);  
                                    } else {  
                                         // channel message eingetroffen  
                                         //irc.senden('PRIVMSG '+empfaenger+" Roger that");  
                                         console.log("wuerden senden: "+'PRIVMSG '+empfaenger+" Roger that");  
                                    }  
                                    // hier können wir nun das Gesagt analysieren und   
                                    // dementsprechend antworten  
                                    // hat jemand geschrieben: sag was,   
                                    // starten wir den Sprüchemodus   
                                    if (antwort.toLowerCase() == "sag was"){  
                                         console.log("sprueche intervall modus aktiv");  
                                         // einen Spruch holen und senden  
                                         irc.sagwas();  
                                    }  
                                    if (antwort.toLowerCase() == "sag sofort was"){                                          
                                         irc.sagwas(true);  
                                    }  
                                    if (antwort.toLowerCase() == "stopp"){  
                                         console.log("sprueche modus deaktiviert");  
                                         irc.zaehler.aktiv = false;  
                                    }  
                                    if (antwort.toLowerCase() == "saso start"){  
                                         console.log("sprueche modus aktiviert");  
                                         irc.zaehler.aktiv = true;  
                                    }  
                                    // wenn jemand fragt, woher die Sprüche kommen  
                                    // geben wir die URL als Antwort  
                                    // diese Bereich kann man gut ausbauen und   
                                    // dem Bot Befehle zum Auswerten von anderen Diensten   
                                    // geben. So dass er die Ergebnisse hier postet  
                                    if (antwort.toLowerCase().match(/woher\s+sind\s+die/)){  
                                         irc.senden("PRIVMSG "+empfaenger+" http://sprueche.woxikon.de");  
                                    }  
                                    // wenn jemand die History haben will ...  
                                    if (antwort.toLowerCase().match(/history:/)) {  
                                         // könnten wir hier diese auswerten und posten...  
                                    }  
                                    break;  
                          }  
                     }  
                }  
           }  
      });  
      cleartextStream.on('end', function(){  
           console.log("session beendet");  
           process.exit(0);  
      });       
      // Objekt, dass uns als zwischenspeicher dient  
      // und auch hilfsfunktionen enthält  
      var irc = {  
           zaehler: {  
                'aktiv':false,  
                'lastword':time()  
                },  
           stream: null,  
           // sendet die Nachrichten an den IRC-Server  
           senden: function(text) {  
                console.log("SEND -", text);  
                this.stream.write(text+'\n');  
                if (text.match(/^JOIN/i)) {  
                     // hole die benutzer  
                     this.senden("NAMES"+text.substr(4));  
                }       
           },  
           // hilfsfunktion, die später für das Chatten genutzt wird  
           sagwas: function(force){  
                if (!force) {  
                     // zufall, wann wieder ein neuer Spruch gesagt wird  
                     var nextcall = Math.round(Math.random()*(10))+3; // minuten  
                     setTimeout(irc.sagwas, nextcall*1000*60);  
                     console.log("nächster spruch ca. in "+nextcall+" minuten");  
                }   
                // nur wenn vor 2 minuten niemand was gesagt hat  
                if (!force && irc.zaehler.lastword > time()-(2*1000*60))  
                     return;  
                if (!force && !irc.zaehler.aktiv)  
                     return;  
                irc.zaehler.lastword = time();  
                // ein paar sprüche seiten  
                var urls = [  
                     {'url':'http://sprueche.woxikon.de/poesiealbum','satz':'Das hier find ich gut:'},  
                     {'url':'http://sprueche.woxikon.de/emo', 'satz':'kennt ihr den?'}];                      
                var pos = Math.round(Math.random()*(urls.length-1));  
                var spruchurl = urls[pos];  
                // hier holen wir die Sprüche ab und   
                // senden diese dann direkt als Message   
                URLLaden(spruchurl.url, function(html) {  
                     // suchen im HTML nach den Sprüchen  
                     var treffer = html.match(/<div\s+class="jingle-content">([\S\s]*?)<\/div>/gi);  
                     var sprueche = [];  
                     for (var a=0;a<treffer.length;a++) {  
                          //console.log(treffer[a]);  
                          sprueche.push(treffer[a].replace(/(<\/?[^>]+>)/gi, ''));  
                     }  
                     // wenn es einige Treffer gibt, wählen wir per zufall einen aus  
                     if (sprueche.length>0) {  
                          //console.log(sprueche);  
                          var pos = Math.round(Math.random()*(sprueche.length-1));  
                          var spruch = sprueche[pos].trim().replace(/\s/," ");  
                          //console.log("kennt ihr den? "+spruch);  
                          irc.senden('PRIVMSG '+config.chans[0]+" "+spruchurl.satz+" "+spruch);  
                     }  
                });                                               
           },  
           users: {}  
      }  
 }  
 function time(onlyseconds) {  
      var datum = new Date();  
      var milliseconds = datum.getTime();  
      if (onlyseconds)  
           return intval(milliseconds/1000);  
      return milliseconds;  
 }  
 // diese Funktion, lädt eine HTML Seite  
 function URLLaden(url, cbfkt) {       
      if (url.match("://")) {  
           var http;  
           if (url.match("s://")){  
                http = require('https'); // die URL verlangt SSL  
           } else {  
                http = require('http');  
           }  
           http.get(url, function(res) {  
            //console.log("Got response: " + res.statusCode);  
            var html = "";  
            res.on("data", function(chunk){  
                 html += chunk; // inhaltteile der Seite zusammen kleben  
            });  
            res.on("end", function(){  
                 // alle Teile sind angekommen und wir haben die HTML-Seite  
                 // cbfkt ist eine anonyme Fkt, die wir aufrufen  
                 // als ergebnis, senden wir das HTML  
                 cbfkt(html);   
            });  
            res.on("close", function(err) {  
                 // verbindung beendet  
                 cbfkt(html);  
                 console.log("Error holen "+url);  
            });  
           }).on('error', function(e) {  
                // verbindung getrennt  
            console.log("Got error: " + e.message);  
           });  
      } else {  
           // falls die URL auf eine lokale Datei zeigt, laden wir diese halt  
           fs.readFile(url, function(err, data){  
                if (err){  
                     console.log(err);  
                } else {  
                     cbfkt(data);  
                }  
           });             
      }  
 }  

Weiterführende Lektüre:

Fazit

So kann man sehr schnell einen eigenen Überwachungsserver schreiben. Dieser meldet einfach den Status im IRC-Chat. Natürlich kann man das beliebig aufbohren. Man sieht aber wieder, wie schnell und unkompliziert man mit NodeJS Lösungen erstellen kann.

Viel Spass

Saso

Montag, 13. Mai 2013

Debugging mit NodeJS

NodeJS Projekte werden immer grösser. Damit wächst auch das Bedürfnis, mal einen richtigen Debugger einzusetzen. So kann man gezielter die Abarbeitung prüfen, ohne viele Loging-Einträge zu erstellen.

NodeJS bietet von Haus aus an, das Programm im Debugmodus zu starten. Es gibt einen ziemlich guten Debugger, der auch optisch sehr gut ist. Leider ist die Dokumentation für das Starten nicht sofort verständlich. Vor allem wenn man noch nie einen Debugger per Konsole gestartet hat.

Debugger installieren

Mittels npm installieren wir den node-inspector.

c:\work\testen>npm i node-inspector

Eventuelle Warnungen beziehen sich auf fehlende Kompilierung. Das schadet aber nicht. Funktioniert dennoch. Dieser lädt die benötigten Dateien und legt diese im Ordner node_modules ab. 

Dort gibt es nun einen Ordner namens node-inspector. Da liegt auch der Debugger als NodeJS Programm.
Der Debugger muss nicht im gleichen Ordner liegen, wie das Programm, welches debugged wird.

Debugger starten

c:\work\testen>node node_modules\node-inspector\bin\inspector.js


Die Ausgabe ist leicht verwirrend. Der Debugger kann nun unter 
http://localhost:8080 
erreicht werden.

Programm debuggen

Ich habe mein NodeJS Programm für das Debugging im Ordner c:\work\freienraumsuchen hinterlegt.
c:\work\freienraumsuchen --debug-brk freienraumsuchen.js


Wichtig ist dass die Option --debug-brk beim Starten mit angegeben wird.
Wenn der Debugger abstürzt. Einfach erneut starten.

Debuggen

Der Debugger hält nach dem Start des Programms automatisch auf der ersten Zeile an. Wir müssen nun das Programm weiterlaufen lassen. Der Debugger sieht aus, wie der Chrome-Debugger.

http://localhost:8080


also auf Play klicken und los gehts.

Fazit

Dieser Debugger ist echt gelungen.
Ich hoffe die Anleitung konnte helfen.

Viel Spass. Gruss Saso

Mittwoch, 24. April 2013

Vorlage für eine Todo-Liste

In Meetings, unterwegs und oft auch direkt am Arbeitsbereich sind Todolisten auf Papier nach wie vor, die besten Helferlein.

Man hat schnell einen Überblick, kann Änderungen schnell vornehmen und alleine durch einen geschwungene Linie Einträge verbinden oder durch Einkreisen gruppieren.

Ich habe oft die Probleme mit der Priorisierung der TODOs gehabt und möchte hier mal meine neueste Vorlage vorstellen.

Die Kopzeile

Prio #ID #NF Titel fällig bis


Prio - Priorität

In Form einer Strichliste stellt dies mit einfachen Mittel die Wichtigkeit dar. Viele Striche, bedeutet hohe Priorität.

#ID - Todo-Nummer

Hier kann man eine fortlaufende Nummer eintragen, damit man auf dieses Todo verweisen kann. Gerade wenn man mit Nachfolger-Todos arbeitet.

#NF - Nachfolger-Todo-Nummer

Hier wird eine TODO-Id für den nächsten TODO-Eintrag eingetragen. Denkbar ist auch, dies umzudrehen und die Spalte mit Vorgänger zu benennen.

Titel - TODO-Eintrag

Hier kommt der eigentliche Eintrag hin. Was soll gemacht werden?

Fällig bis - Datum 

Ein Datum eintragen, bis wann dieser Task gemacht werden sollte.

Benutzen

Wenn möglichst verwendet man einen Kugelschreiber, der mehrere Minen mit verschiedenen Farben hat. So kann man die Einträge gleich durch die Farbe gruppieren.

Das Eintragen erfolgt so wie die Ideen einem in den Kopf kommen. Die Verbindungen zwischen Tasks (TODO-Einträgen) wird über die #NF gemacht. Wobei man immer eine Linie zwischen Elementen ziehen kann. 

Sehr wichtige Einträge kann man auch einkreisen oder am besten mit einem kleinen passenden Symbol hervorheben. Ideal sind auch kleine Kritzeleien zum Thema. Hier schlägt Papier die TODO-Liste am Computer. Wobei durch die Tablet-PCs neue Spieler auf dem Feld erschienen sind.

Wenn ein TODO-Eintrag erledigt ist. DURCHSTREICHEN. Das gibt auch einen Geschafft-Effekt. Wichtig für die eigene Motivation.

Beispiel

Prio #ID #NF Titel fällig bis
III 1 3 Bloggen über TODO
2 Einkaufen 24.04.13
I 3 Twittern über TODO

Hoffe diese Anregung hilft dem Einen oder Anderen. Vielleicht inspiriert dies zu einer eigenen, noch besseren Todo-Liste.

Viel Spass damit.

Dienstag, 23. April 2013

Firmengründung für alle

Klingt erstmal provokant. Ich will kurz skizzieren wie das möglich ist.

Die Anregungen stammen aus:

Wenn man beide Bücher zusammen mischt und die Doppelungen entfernt, hat man alles was man zum erfolgreichen und einfachen Start ins eigene Unternehmen braucht. Und das mit minimalsten Aufwand. Minimaler Aufwand beim Gründen und vor allem beim Betreiben.

Ich leihe mir sehr gute Begrifflichkeiten aus beiden Büchern und versuche kurz ein Vorgehensmodell zu skizzieren.

  1. Man kommt mit einer vagen Idee.
    Die Idee muß einem Spaß machen! Dabei sind Ideen am besten aus eigener Not oder Wunsch heraus entstanden. Bsp: Bessere Schuhe zum Klettern, günstigerer Tee, einfacher Verkauf von Dateien, etc.
  2. Die Idee sollte etwas Bestehendes verbessern. Anderes billiger machen oder einfach mehr Leben ins Leben bringen.
  3. Dann versuchen Sie die Idee im Kopf zu optimieren, indem Sie ausformulieren, was das Beste an der Idee ist. Warum braucht man gerade Ihre Idee.
  4. Die wertvollen Vorteile hervor heben. Ermitteln Sie ehrlich, was die Konkurrenz machen wird, um gegen Sie anzutreten. 
  5. Dann nochmal die Idee gedanklich ausbauen und noch mehr einen Vorteil ausarbeiten.
    Wichtig: Halten Sie Ihre Idee einfach her vom Produkt. Also nur 1 Produkt anstatt sehr viele. Konzentrieren Sie sich auf einen Kerndienst Ihrer Idee und spitzen Sie das Angebot zu.
    Damit haben Sie viele Vorteile.
  6. Dann malen wir den harten Weg auf. Am besten als Schaubild.
    Bsp: Produkt in China bestellen. Dort abholen. Ins eigene Lager. Verkaufsräume mieten. Personal einstellen. Für die Logistik gleich ein Büroteam aufbauen. Buchhaltungsteam einstellen. Rechtsabteilung. Marketing organisieren. Etc.
Nun gehen wir an die Vereinfachung. Wir betreiben Komponentenbasierte Gründung.
  1. Zuerst Minimarktforschung: Internetseite mit den vielen kostenlosen Tools aufsetzten.
    1. 1 bis 2 Seiten genügen. 
    2. Produkt beschreiben. 
    3. Bestellmaske rein.
      Wenn einer diese ausfüllt, wird angezeigt: "Derzeit nicht mehr vorrätig. Wir melden uns."
    4. Die Statistik der Seitenaufrufe zeigt später ob Interesse besteht.
  2. Nun für 25 Euro Werbung (google adwords). Und den Markttest starten.
Sind Kunden interessiert? Dann los. Wir erstellen nun das System.

  1. Die Posten auf dem Schaubild werden durch Komponenten ausgetauscht. 
    1. Produktion wird an den Fabrikant als Auftrag gegeben.
      Viele arbeiten auch on-demand. Je nach Produkt.
      Bei teuren Sachen nehmen Sie Vorkasse vom Kunden und bestellen erst dann.
  2. Unzählige Fullfilment-Dienstleister. Wählen Sie einen.
  3. Bestellung werden auf den Dienstleister umgelenkt.
    Diese liefert auch aus.
  4. Produzierte Produkte gehen ins Lager vom Dienstleister. Etc.

Optimieren Sie solange bis es passt, leicht und günstig geworden ist.
Hier sind digitale Produkte natürlich von Vorteil. Aber Videos oder so auch. Schulungsvideos, Lehrbücher, Seminare. Kongresse. Denken Sie und lassen Sie ausführen.

Wichtig:
Investieren Sie so wenig wie möglich. Machen Sie ein Hobby draus. Wenn es mal nicht klappt haben Sie nur sehr wenig Geld verloren und Zeit. Aber hatten Spaß.

Viel Spass beim Ausprobieren.

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.