Samstag, 14. Dezember 2013

Spontane Abstimmungen durchführen - mit kostenloses Online Tool TEDzi

Sie haben eine Frage mitten im Vortrag gestellt. Keiner der Anwesenden möchte sich outen und für eine der Optionen stimmen. Was nun? Alles umstellen und jedem eine Karte austeilen? Einsammeln und dann die Auswertungen am Flipchart selbst vornehmen?

Ja, für einige ist das ein Teil der Moderation. Für andere aber ein Hindernis.
AdHoc eine TED (Abstimmung) starten, das wäre jetzt nicht schlecht. Alle Anwesende verfügen über ein SmartPhone und Internet. Warum nicht dies nutzen?

Ob Hochschule oder private "Wer wird Millionär"-Runde. Ein eigenes TED-System ist nützlich. Bei www.tedzi.com kann man ohne Registrierung eine TED-Session eröffnen. Die Teilnehmer verbinden sich über den TED-Session Code, der dem Moderator angezeigt wird.

Man kann beliebig viele TEDs starten. Das geht total einfach. Mit dem Daumen auf dem Smartphone, aber auch im Browser. Die Webseite ist für das Display eines Smartphones optimiert und damit muss man auch keine App installieren.

Die Optionenwahl bietet alles:
- Ja, Nein, Vielleicht, Enthaltung
- Option A..F
- Werte 1..12

Die Abstimmung kann im Singlemode laufen, also nur eine Wahlmöglichkeit anwählbar für die Teilnehmer, oder auch Multiplichoice TEDs sind möglich. Die Entscheidung kann jederzeit im laufenden TED mit einem Klick geändert werden.

Die Benutzerführung ist sehr aufgeräumt und intuitiv. Schön ist auch die Möglichkeit die Auswertung in der laufenden Abstimmung an alle Teilnehmer live zu senden. Damit kann man sich leichter für eine Option entscheiden, wenn es um einen Termin oder eine Location geht.

Probiert es aus:
www.tedzi.com

Sonntag, 8. September 2013

Schluss mit der unmenschlichen Politik in Deutschland

Die Regierung besteht nur aus bestochenen und geschmierten Politikern. Der Wähler wird nicht gefragt und belogen solange es geht und darüber hinaus.

Bestechungen und Lobbytum ist sehr einfach. In Deutschland wird die Macht auf wenige nicht direkt gewählte Personen verteilt. Ihr Handeln ist durch Immunität geschützt.

Deutschland hatte genug Führer. Wir brauchen Volksvertreter. Politiker sollten Verwaltungsbeamte sein, die den Volkswillen umsetzen. Dieser Wille kann heute mit den neuen Mitteln sicher und zuverlässig ermittelt werden. Politiker sollten die von der Mehrheit erwünschten Themen gesetzeskonform formulieren und Umsetzung verantworten.

Der egoistische Wille eines Einzelnen sollte nicht unser Leben bestimmen. Es gibt immer wieder Menschen, die Wähler als unmündig und als Untermenschen ansehen, welche nicht richtig entscheiden könnten und die Tatsachen nicht verstehen würden. Diese selbsternannten Übermenschen wollen uns vor uns selbst schützen. 

Genau dies findet in Deutschland statt. Wir sollten dieses Verhalten beenden. Alle Macht der echten und wahren Demokratie. Ich bin für die direkte Demokratie und die Abschaffung der über bezahlten Lobby-Politiker und das marode Anti-Demokratie System.

Dienstag, 27. August 2013

Scrum for One - Scrum alleine einsetzen Teil 3

Wie kann man Scrum auch alleine einsetzen?
Hier möchte ich meinen Weg aufzeigen. Mit den Scrum Mittel kann sich jeder sein eigenes System erstellen.

Startphase

Der Auftraggeber hat eine Idee von einem Produkt. Zuerst wird das Gesamtbild, das Endprodukt besprochen. Ich lasse mir als die Vision erklären.
Sollte der Auftraggeber keine Userstories haben. Lege ich für das Projekt welche an. Wir besprechen die Userstories. So kann der Auftraggeber nochmal prüfen, ob sein Produktwunsch verstanden wurde.

Release-Planung

Wenn die Userstories besprochen sind, ordnet der ProduktOwner (Auftraggeber) diese in eine Reihenfolge. Ich erstelle dann ein Produkt-Roadmap. Dazu reicht oft ein DIN A4 oder A3 Blatt aus. Diese ist immer präsent aufgehängt oder liegt am Arbeitsplatz. Damit kann man gut den Focus auf das Projektziel halten. Das verhindert auch mehr Arbeit, die nicht bestellt wurde.

Schätzung der Userstories

Nach der Schätzung der Userstories kann ich die eigentliche Arbeit beginne. Bevorzugt in einem 1-Wochensprint oder 2 Wochen Sprint. Da in diesen kurzen Abständen, das Feedback schneller eingebunden werden kann. Besonders dann wichtig, wenn der Kunde selbst nur eine vage Vorstellung vom End-Produkt hat.

Sprint vorbereiten

Die Userstories werden in der Reihenfolge abgearbeitet. Für die Tasks nehmen ich DIN A4 Blätter. Jede Userstory hat sein eigenes Blatt. Das Blatt liegt beim Arbeiten im Querformat vor der Tastatur und erlaubt mir immer einen schnellen Einblick. Die Userstory kommt als Überschrift als Stichpunkt auf das Blatt oben. Wenn ich an mehreren Projekten arbeite, schreibe ich meist auch ein Kürzel für das Projekt mit dazu. So kann man gut die Task-Blätter organisieren. Auf ein ScrumBoard verzichte ich damit. Vor mir liegt die Produkt-Roadmap und die Blätter mit den Userstories. 

Die Userstories stappel ich. Damit bleibt der Focus auf das aktuelle Abarbeitungspaket. Auf jedem Blatt erfasse ich die nötigen Schritte für die Umsetzung. Das sind die Tasks. Wenn die Reihenfolge im Laufe der Entwicklung geändert wird, schreibe ich mir Zahlen an die Tasks. Wenn es zu unübersichtlich wird, schreibe ich das Blatt einfach neu. 

Durch das Erfassen der Tasks gehe ich den Entwicklungsprozess der Userstory im Kopf durch. Dabei fallen mir Arbeitsschritte ein, die ich vorher vergessen habe zu berücksichtigen. Im Laufe der Entwicklung kommen auch immer wieder Tasks dazu oder manche werden entfernt. Das Entfernen demonstriere ich durch das Durchstreichen mit wellenartige Linien. So kann ich abgearbeitete Tasks von verworfenen gut trennen.

Zeitschätzung jederzeit möglich

Da sich die Tasks bei mir tatsächlich immer im gleichen Zeitbereich bewegen, kann ich auch schnell eine Aussage über die Dauer geben. Somit kann ich die eigentliche Schätzung gut anpassen. Ich zähle einfach die Tasks und multipliziere diese mit meiner durchschnittlichen Zeit pro Task.

Sprint machen

Wenn die Tasks stehen, geht es los. Ich arbeite die Tasks der Reihe nach ab. Daten und Informationen die ich benötige und mir im Laufe der Entwicklung einfallen, werden direkt auf das Blatt geschrieben. So bleibt alles zusammen.

Jeder erledigte Task wird durchgestrichen. Das gibt eine gutes Gefühl und motiviert gleich zu weiteren Arbeiten. Die Tasks selbst sind in der Regel auch kurz und klein gehalten. Die umgesetzten Tasks werden wenn möglich sofort in ein Quellcode-Versionierungssystem eingecheckt. Damit bleiben die Änderungen erhalten und man kann schnell wieder den alten Zustand herstellen.

Daily Scrum

Ist nicht wirklich nötig. Da ich alleine an dem Projekt arbeite. Aber für den Beginn der Entwicklung sichte ich meist nochmal die erledigten Tasks und reflektiere nochmals was zu tun ist. Auch ein Blick auf die Gesamtstory ist sehr vorteilhaft, also die Roadmap ansehen.

Sprint Review

Das Sprint Review mache ich mit dem Kunden entweder gemeinsam, Live per Skype oder er sendet mir seine Anmerkungen per Email zu. Das Inkrement stelle ich dafür auf meinen Testserver und sende ihm den Link zu. Rework kommt in die nächste Sprintplanung. Damit kann ich dann auch Verlängerungen in der Projektdauer gleich kommunizieren.

Sprint Retrospektive

Ist etwas schwierig alleine. Da kann helfen, den Kunden zu fragen, ob er noch eine andere oder verbesserte Kommunikation wünscht. Für mich versuche ich schon während der Entwicklung, Optimierungen gleicht anzuwenden. Motto: Man lernt nie aus. Dafür lese ich mich immer wieder in Bücher für besseres Programmieren und Testen ein.

Fazit

ScrumForOne geht. Man muss etwas disziplinierter sein, aber kann schnell gute Erfolge erzielen. Viel Spass mit diesen Anregungen.

Freitag, 16. August 2013

Scrum for One - Scrum alleine einsetzen Teil 2

Definition of Done

Sie erhält Einschränkungen und Anforderungen die vom ganzen Team als globale Qualitätskriterien angesehen werden. Jeder Backlog-Eintrag kann zusätzliche Anforderungen für die Abnahme erhalten. Im Laufe der Entwicklung fordert das Scrum Team immer mehr Kriterien für die Qualität von sich selbst. Diese werden dann in die "Definition of Done" eingetragen.

Die "Definition of Done" (DoD) wird gut sichtbar aufgehängt und ist dem Scrum Team bekannt. Die DoD wird zwischen dem  ProductOwner und dem Entwicklungsteam ausgemacht. Nur Backlog-Einträge die alle DoD Akzeptanzkriterien erfüllen, werden als "fertig" betrachtet.

Daily Scrum

Damit bringt sich das Entwicklungsteam auf den neuesten Stand. Diese Meetings sind sehr kurz und werden täglich, bzw jeden Entwicklungstag abgehalten. Jedes Team-Mitglied beantwortet drei Fragen:
  • Was habe ich seit der letzten Besprechung erreicht?
  • Was wird bis zur nächsten Besprechung erledigt?
  • Welche Hindernisse stehen im Weg?
Die Beantwortung geht reihum und jeder sollte sich kurz halten. Maximal 1 Minute pro Person. Weitere Gespräche werden nur mit den betroffenen Personen im Anschluss des Meetings gesondert geklärt. Um es für alle einfach zu machen, sollten die Meetings immer am gleichen Ort zur gleichen Zeit stattfinden. Nur das Scrum Entwicklungsteam darf sprechen. Der Scrum Master moderiert.

Anschließend werden Aktualisierungen gemacht am:
  • Sprint Backlog
  • Sprint Burndown
  • Impediment Backlog (Hindernisse)
Das Impediment Backlog wird vom Scrum Master abgearbeitet.

Sprint Burndown

Dieser Chart zeigt den Fortschritt des aktuellen Sprints. Auf der vertikalen Achse wird die verbleibende Arbeit eingetragen. Auf der horizontalen stehen die Arbeitstage. Das Einzeichnen erfolgt im Daily Scrum.

Inkrement

Das Inkrement stellt den aktuellen Zustand eines Programmes dar. Alle Ergebnisse aus den erledigten Sprints sind enthalten. Nach jedem Sprint entsteht ein neues Inkrement. Das Inkrement muss funktionieren, bzw. benutzbar sein. Die Qualität entspricht immer der "Definition of Done"

Sprint Review

Hier wird das neue Inkrement abgenommen. Das Sprint Review wird am Ende eines Sprints gemacht. Das Entwicklungsteam führt das Inkrement vor. Der Fokus der Produktvorführung liegt auf den Sprintzielen. Die Präsentation erfolgt nur für tatsächliche Ergebnisse. Keine "ist eigentlich fertig" oder Bilder und Folien. Die Stakeholder und das Scrum Team besprechen die Ergebnisse und beschliessen den Nächsten Schritt.

Sprint Retrospektive

Im Anschluss an das Sprint Review analysiert das Scrum Team den abgearbeiteten Sprint. Verbesserungsvorschläge für das Arbeiten werden ermittelt und für die folgenden Sprint festgehalten.

Grooming

Vor dem Start der Planung des nächsten Sprints wird das Product Backlog durch den Product Owner auf den neuesten Stand gebracht. Änderungen und Reihenfolgenanpassungen werden erläutert. Das ganze Team erfährt die neuen Anforderungen. Das Entwicklungsteam muss die neuen UserStories schätzen. Auswirkungen auf das weitere Vorgehen und eventuell neue Anforderungen an "Definition of Done" werden erörtert.

Release Burndown

Der Product Owner ist für das End-Produkt verantwortlich. Er bestimmt die tatsächlichen Features und den Umfang des Produkts. Die Product Backlog-Einträge kann er in einen Release Plan eintragen. Wenn die UserStories geschätzt sind und die in der Regel gleichbleibende Anzahl an Storypoints abgearbeitet werden, dann kann der Product Owner einen Releaseplan erstellen. Die kompletten Storypoints werden in Sprints umgerechnet. Die Sprints werden gezählt.

Ähnlich wie der Sprint Burndown wird auch der Release Burndown erstellt. Vor jedem Sprint trägt der Product Owner die zu erreichenden Storypoints ein.  Auf der vertikalen Achse steht die verbleibenden Arbeit für das Release. Auf der horizontalen Achse stehen die Sprints. Damit kann die Abarbeitungsgeschwindigkeit ermittelt werden. Je mehr Sprints gemacht wurden, desto genauer wird die Zeitplanung.

Im Teil 3 folgt:

Umsetzung von Scrum mit nur einer Person

Montag, 12. August 2013

Scrum for One - Scrum alleine einsetzen Teil 1

Scrum ist eine sehr gute Sache. Ideal für Teams ab 3 Personen. Was machen Freelancer und Entwickler die für sich selbst arbeiten?

ScrumForOne nenne ich die Scrum-Prinzipien auf eine Person umgewandelt. Dabei kann man die Zyklen eines Scrum-Ablaufs auch alleine machen. In meinen nächsten Posts beschreiben ich die einzelnen Teile und wie man Scrum auch alleine machen kann.

Scrum Zyklen

Am Anfang ist der Projektauftrag. Der Produkt-Owner formuliert diesen mittels Userstories. Die Sortierten Userstories stellen das Produkt Backlog dar. Pro Iteration werden einige Einträge aus dem Produkt Backlog in einem Sprint abgearbeitet. Der Sprint hat eine fest Dauer von 2-4 Wochen. Nach dem Sprint wird das erstellte Werk begutachtet und die Planung für den nächsten Sprint kann beginnen. 

Vor der Planung wird die Retrospektive gemacht. Das Team reflektiert den vergangenen Sprint und seine eigene Arbeitsweise. Was war gut, was war weniger gut, was machen wir nun damit wir uns verbessern?
Die weiteren Zwischenschritte und die Umsetzung, wenn man ScrumForOne macht folgen in den nächsten Beiträgen.

Scrum Team besteht aus mehreren Rollen

Product Owner (Produktbesteller, der entscheidet was das Produkt demnächst braucht)
Scrum Master (Moderator und Coach für das Vorgehen im Scrum)
Scrum Team (die Umsetzer)

Alle Scrum Rollen in Einer

In der Regel sollte keine Rolle mit der gleichen Person mehrfach belegt sein. Bei ScrumForOne ist das genau umgekehrt. In der Regel sind alle Rollen mit dem Freelancer besetzt. Idealerweise hat setzt der Freelancer Kundenanfragen um, dann ist er ProductOwner der Auftraggeber. Bei eigenen Projekten ist man alles.

Ohne Disziplin geht Scrum nicht

Diese Art der Umsetzung von Scrum erfordert etwas Disziplin. Man muss sich einen Plan erstellen. Man muss sich an den Plan halten. Man muss so sich selbst reflektieren und verbessern. Ja, man muss sich selbst verbessern wollen.

Anforderungen einsammeln bevor Scrum beginnt

Egal ob im Team oder nicht, man braucht einen Plan. Schreiben Sie die Anforderungen an die Software als Userstories. Bringen Sie die Userstories in eine Reihenfolge. Versuchen Sie die Userstories unabhängig voneinander zu gestalten. Userstories sollten nicht von anderen Userstories abhängig sein. Setzen Sie die wichtigsten Userstories an den Anfang. Die wichtigsten Userstories sind diejenigen, welche das Produkt in der einfachsten Version benötigt und Ihnen eine erste Veröffentlichung ermöglichen könnte.

Wie erstellt man Userstories?

Jede Userstory enthält eine Anforderung mit Nennung des Grundes und von wem diese Anforderung stammt. Der Aufbau folgt:

  • Wer möchte welches Feature, damit Was erreicht wird
  • Als Nutzer will ich Ziel/Wunsch damit Nutzen entsteht
Beispiele:
  • Als User benötige ich eine Kontoübersicht, damit ich meinen Kontostand einsehen kann
  • Als Administrator benötige ich eine Liste mit geblockten Accounts, um schneller einen Überlick zu erhalten
  • Der Service benötigt eine Backup-Option, um Datensicherungen von Kunden erstellen zu können
Halten Sie den Aufbau kurz und präzise. Man kann die Userstories schrittweise verfeinern und in detailiertere Userstories aufsplitten.

Die Userstory erklärt auch den Grund für das Bedürfnis.

Damit kann der Entwickler besser verstehen, was mit diesem Feature erreicht werden soll. So ist er in der Lage die erwartete Funktionalität optimaler umzusetzen. Es entstehen weniger Missverständnisse.

Product Backlog von Scrum

Alle Elemente in Form von Userstories stellen das Product Backlog dar. Diese Elemente sind nach Wichtigkeit für den Product Owner sortiert. Sie beschreiben das ganze Produkt. Anpassungen an der Reihenfolge wird durch den Product Owner immer wieder vorgenommen. Wenn neue Anforderungen entstehen oder gar wegfallen, wird das Product Backlog durch den Product Owner angepasst.

Product RoadMap bietet Übersicht des kompletten Produkts

Die Product RoadMap enthält das Product Backlog. Die Backlog Einträge werden hier in Pakete gepackt. Diese Packet entsprechen den Versionen des Products. Jedes Packet kann in mehrere Bereiche unterteilt werden. In diese Bereiche kommen die Product Backlog Einträge. Damit kann man schnell erkennen, was das Produkt wann können wird. Dabei zeichnet man am Besten eine Kreuztabelle.

Beispiel:

Nutzerverwaltung Verkauf ...
Skeleton Kunden erstellen, Löschen, Bearbeiten
Kunden listen
...
Version 1 Kunden sperren Newsletter erfassen
Newsletter versenden
Shop verlinken, Affiliate tracken
...
...

Scrum Sprint

Die Dauer für einen Sprint wird für das Projekt komplett festgelegt. Sollte zwischen 2 und 4 Wochen sein. In einem Sprint werden Backlog Einträge umgesetzt. Am Ende des Sprints werden die Ergebnisse übergeben.

Das Sprint beginnt mit einer Sprint Planung 1 und Sprint Planung 2. Anschließend werden die abgeleiteten Task aus dem Sprintziel umgesetzt. Jeden Tag wird ein Daily Scrum durchgeführt. Dabei werden die geschafften Tasks erwähnt und die nächsten Tasks genannt. Probleme werden ebenfalls in den Problemspeicher notiert. Der Problemspeicher enthält Störungen und andere Probleme, die beseitigt werden müssen, damit die Umsetzung nicht gestört wird.

Wenn der Sprint fertig ist, werden die erstellten Teile präsentiert. Nicht erstellte Teile oder unfertige Tasks und Produckt Backlog Einträge werden nicht präsentiert. Diese wandern wieder in den Produkt Backlog und werden in der Sprint Planung 1 vom Produkt Owner neu priorisiert.

Scrum Sprint Planung 1

Der ProduktOwner stellt seine Produkt Backlog.Einträge vor. Hier wird die gesamte Produktvision und Story nochmal als Ganzes gesehen. Wenn es Änderungen an den Einträgen gab oder diese umsortiert wurden, wird hier nochmal besprochen ob es Auswirkungen auf das gesamte Produkt gibt.

Die Abhängigkeiten und das eigentliche Produktziel soll im Hinterkopf behalten werden, damit während der Entwicklung entsprechend programmiert wird. Mit diesem Hintergrundwissen kann man leichter Entscheidungen für eine bestimmte Variante oder Vorgehensweise treffen.

Nun werden die umzusetzenden Einträge festgelegt. Dabei werden in Abhängigkeit von der Sprintdauer, die Produkt Backlog-Einträge als Sprintziel definiert. Diese müssen in ein Sprint realisiert werden und ergeben das Sprint Backlog.

Scrum Sprint Backlog

Das Sprint Backlog besteht aus den Produkt Backlog Einträgen, die der Produkt Owner umgesetzt haben möchte und die Entwicklung realistisch in der Zeit eines Sprints umsetzen kann. Die Bewertung der Dauer pro Backlog Eintrag wird durch die Entwickler geschätzt. Bei ScrumForOne sollte man sich bewusst die Einträge ansehen und nur nach der Schätzung auswählen. Nicht nach dem eigenen Wunsch was man alles realisieren will. Das sollte über die Priorisierung geklärt werden.

Scrum Sprint Ziel

Beschreibt was am Ende des Sprints realisiert werden soll. Dabei ist es das Ziel alle Sprint Backlog-Einträge umzusetzen. Das Sprint Ziel kann aber nochmal als Satz formuliert werden. Was wird in dieser Iteration erreicht? Das hilft den Focus zu behalten und kann auch für die Aussenkommunikation genutzt werden. Beispiel: Wir arbeiten gerade an der Realisierung der Bestellabwicklung.

Scrum Sprint Planung 2

Die Einträge aus dem Sprint Backlog werden in kleinere Tasks umgeschrieben und entsprechend in der Umsetzungsreihenfolge erfasst. Durch diese Übersicht, verliert man das Ziel nicht aus den Augen und verhindert, dass man sich in kleine Aufgaben verzettelt. Auch werden so nicht immer wieder neue Baustellen begonnen. Die Vorbereitung der umzusetzenden Aufgaben ist sehr wichtig.

Im Teil 2 folgt:

Definition of Done
Daily Scrum
Sprint Burndown
Inkrement
Sprint Review
Sprint Retrospektive
Grooming
Release Burndown


Mittwoch, 17. Juli 2013

Lernen für Klausuren mit Struktur

Das Lernen für Klausuren ist immer ein elendes Thema. Dabei kann es Spass machen und einfach sein, wenn man strukturiert vorgeht. Hier ist mein Vorgehen, dass mir gut Dienste geleistet hat.

Vorteile


  • Man gewinnt Zeit
  • Befasst sich mit dem Stoff mehrmals
  • Es bleibt was hängen
  • Für die Klausur wird der "Spickzettel" meist nicht mehr benötigt, da die Infos sich in das Gehirn eingebrannt haben

Vorgehen

  1. Überblick verschaffen
  2. Lerneinheit lesen und sich Stichpunkte mit einer Erklärung aufschreiben. Stichpunkte können auch unterstrichen werden und die Erklärung folgt nach dem Doppelpunkt
  3. 2 Tage später diese Zusammenfassung durch Umformulierungen auf eine halbe DIN A4 Seite bringen
  4. Vor der Prüfung, also in der Lernphase diese halben Seiten nochmal um die Hälfte reduzieren
  5. Diese reduzierten Bereiche auf die "Spickzettel" übertragen
  6. Beim Übertragen kann man sich die Bereiche entsprechend arrangieren, dass beim darauf schauen eine Gedankenstütze sein kann
  7. Jeden Tag diese Informationen durch Umformulierungen kürzen und damit auf immer weniger Blätter verteilen
  8. Vorgang wiederholen bis 2 Seiten DIN A4 alle Informationen enthalten
  9. Keine Rückseite nutzen. Damit kann man die komplette Information auf dem vor sich Tisch liegen haben, ohne Blättern zu müssen
  10. Extra-Tipp: Lernen auf dem Weg zur Arbeit oder Schule.

Überblick verschaffen

Lesen Sie die Kapitelüberschriften. Sehen Sie sich die Seitenzahlen an. Verschaffen Sie sich einen Eindruck wie viel Material auf Sie zu kommt. In der Regel ist ein Kapitel eine Lerneinheit. Rechnen Sie die Zeit zur Klausur hoch und erstellen Sie sich einen Lernplan. Abhängig von der Zeit und der Menge an Material könnte es sein, dass Sie 1 Woche pro Lerneinheit einplanen wollen. 

Lassen Sie mindestens 1 Woche extra Zeit für die Lernphase vor der Klausur frei.

Lerneinheit lesen

Lesen Sie zuerst die Überschriften aus dem Inhaltsverzeichnis. Wenn es einen Zusammenfassung (oft am Ende des Kapitels) gibt, lesen Sie diesen auch. Sie erhalten damit ein gutes Gefühl was dran kommen wird und welche Fragen hier nicht vertieft werden.

Damit verschaffen Sie sich einen besseren Überblick, bevor Sie loslegen und sich Notizen machen während Sie lesen. Manche Teile erfordern mehr Platz und andere sind nicht mal interessant genug für eine Randnotiz.
Merksätze und andere hervor gehobene Passagen sollten Sie auf Ihr Blatt übertragen. 

Schreiben Sie einen Stichpunkt auf. Unterstreichen Sie diesen. Stichpunkte können auch kleine Sätze sein. Diese können Sie in späteren Schritten auf einzelne Wörter reduzieren, wenn Sie mehr mit diesem Begriffen anfangen können. Formulieren Sie nun die gelesene Information in kurzen Sätzen und schreiben Sie diese zu Ihrem Stichwort.

Nehmen Sie für jede Lerneinheit neue Blätter und verwenden Sie nicht die Rückseite. Damit müssen Sie nicht immer die Seiten umdrehen und vermeiden wildes suchen. Durch blosses Ausbreiten der Seiten haben Sie alles im Blick.

Zusammenfassung Ihre Notizen

Nachdem nun mind. 1 Tag aber maximal 3 Tage vergangen sind sollten sie diese erfassten Notizen nochmal zusammen fassen. Dadurch brennt sich das gelernte besser in Ihr Hirn ein. Setzen Sie sich einen Lerntermin!

Ziel ist es die Informationen Ihrer Notizen auf eine ungefähr eine halbe DIN A4 Seite zu reduzieren. Wenn es weniger ist, auch gut. Manche Lerneinheiten enthalten soviel allgemeines, dass Sie diese Informationen nicht mehr lernen müssen. Sparen Sie sich das Erfassen solcher Informationen.

Überdenken Sie die Stichpunkte. Eventuell können Sie diese nun stärker reduzieren. Überlegen Sie sich auch eine sinnvolle Reihenfolge für die Informationen. Durch das geschickte Umsortieren können Sie sich das Aufschreiben von Informationen ersparen, da durch die Reihenfolge der Sinn klar wird.

Formulieren Sie Ihre Erklärungssätze neu und reduzieren Sie den Inhalt auf das Wesentliche. Dieser Denkprozess ist der wichtigste. Sie verarbeiten die Information neu und es wird immer tiefer in Ihr Gedächtnis getragen. Lassen Sie Füllwörter weg. Entwickeln Sie Ihre eigenen Abkürzungen.

Spickzettel beginnen

In der Lernphase oder schon vorher sollten Sie Ihre Notizen zu den Lerneinheiten nochmals um die Hälfte reduzieren. Schreiben Sie diese Informationen dann auf die Spickzettel. Sortieren Sie die Blöcke mit den Informationen, so dass Sie sie beim Erinnern der Informationen unterstützen. Bilden Sie logische Abfolgen auf Ihren Spickzetteln.

Reduzieren ist Lernen

Reduzieren Sie die Informationen jeden Tag ein Stück. Ziel ist es alle Informationen auf maximal 2 Seiten DIN A4 zu bekommen. Gehen Sie aber nicht zu schnell vor. Der Weg ist das Ziel. Das Reduzieren und Umformulieren der Informationen ist das Lernen.

Keine Rückseite

Verwenden Sie nicht die Rückseiten. Dadurch bremsen Sie sich nur aus. Um an die Informationen auf der Rückseite zu gelangen, müssen Sie das Blatt wenden und damit verlieren Sie die Informationen von der Vorderseite aus den Augen. Während der Klausur können Sie diese Seiten auch direkt vor sich legen und haben immer alles im Blick. (Sofern es zu Ihrer Klausur gestattet ist)

Lerntipp

Nehmen Sie Ihre Spickzettel mit. Lesen Sie die Stichpunkte und versuchen Sie für sich selbst die Erklärung dazu vorzusagen. Prüfen Sie dann Ihre Notizen. Am besten merken Sie sich Geschichten. Wenn Sie da Wissen (die Erklärung) auf die reale Welt umsetzen können, merken Sie sich diese Geschichte.

Viel Spass
Saso Nikolov

Donnerstag, 27. Juni 2013

Texte effektiver formulieren - Erreichen Sie dadurch mehr Akzeptanz bei Ihren Lesern

Nutzen Sie die aktive Stimme

Ausschreibende Formulierungen geschäftlicher Texte, wie Geschäftsbedingungen, Vertragsbestandteile, Verkaufsprospekte und andere, lässt die Leser nach klaren Aussagen und kurzen Sätzen betteln. Der schnellste Ansatz einer Korrektur liegt in der Nutzung der aktiven Stimme mit starken Verben. Denn es befinden sich oft viele Formulierungen mit "zu haben" oder "zu erreichen" in den Sätzen

Setzen Sie starke Verben ein

Schwache Verben werden oft durch zwei weitere, grammatikalisch unerwünschte Verben begleitet. Die Passive Stimme und Ihre Hilfsverben. Beide zusammen verlängern und verkomplizieren diese den Satz unnötig.

Aktive und Passive Stimme

Beispiel der aktiven Stimme:
Der Vater weckt seine Tochter.

Beispiel passiver Stimme:
Die Tochter wird vom Vater geweckt

Die passive Stimme benötigt Hilfsverben. Das eigentliche Subjekt rückt in den Hintergrund. Die ausführende Person wird durch das Wort "vom" eingeführt. Manchmal wird auch der Ausführer weggelassen. Dann kann man nicht mehr erkennen wer was macht.

Verbannen Sie nicht die passive Stimme, nutzen Sie diese sparsam

Die passive Stimme macht durchaus Sinn. Untergeordnete Personen können damit in den Vordergrund des Satzes gebracht werden. Nutzen Sie die passive Stimme nur, wenn Sie einen guten Grund haben. Im Zweifel wählen Sie die aktive Stimme.

Vermeiden Sie überflüssige Wörter

Wörter sind überflüssig, wenn diese durch weniger Wörter ersetzt werden können, die das Gleiche bedeuten. Manchmal gibt es einfachere Wörter für bestimmte Formulierungen

Statt: "um zu erreichen"
Besser: "damit"

Schreiben Sie positiv

Positive Sätze sind kürzer und leichter zu verstehen, als die negativen Gegenstücke. Zum Beispiel:

vorher
Anders als die Hauptakteure werden die B-Aktionäre vermutlich keine Dividenden erhalten.

nachher
Nur die Hauptakteure erhalten vermutlich eine Dividende.

Die Sätze weden kürzer und leichter verstanden, wenn Sie negative Teile durch ein einziges Wort, welches das Gleiche bedeutet, ersetzen.

Beispiel:
Nicht möglich wird zu unmöglich

Fazit 

Es ist durchaus möglich verständliche Texte zu erstellen. Diese sollen helfen, effektiver den Textinhalt zu erfassen. Dadurch gewinnt der Leser Zeit und kann sich auf Entscheidungen konzentrieren. Gerade Unterlagen und Schriftverkehr im geschäftlichen Alltag sollten etwas optimierter verfasst werden. In meinem Buch habe ich noch viel mehr Beispiel und Anleitungen zusammen gestellt. 

Buchempfehlung

Klare Texte schreiben - Verständliche Geschäftsunterlagen und Dokumente erstellen
Dieses Werk zeigt Ihnen, wie Sie einfache und klare Texte verfassen. Dabei sollen nicht die komplexen Sachverhalten vereinfacht werden, sondern die Art und Weise, wie diese präsentiert werden. Schreiben Sie Texte die auch verstanden werden.

Mittwoch, 19. Juni 2013

FFMPEG - Thumbnails aus Filme erstellen und Filme schneiden mit ffmpeg

Mit ffmpeg kann man schnell die meist benötigten Videoarbeiten ausführen. Die Parameterflut ist jedoch schnell unübersichtlicht. FFMPEG kann super viel.

Kleine Auswahl was FFMPEG kann:


  • Streaming Server
  • Video schneiden
  • Thumbnails, also Screenshots heraus holen
  • Videoaufnahme
  • Videos konvertieren, also in andere Formate umwandeln

FFMPEG für jedes Betriebssystem

Man kann die Quelldateien herunter laden und alles selbst kompilieren. Oder man lädt nur die ausführbare Datei für sein eigenes Betriebssystem. Ich empfehle fertig herunter laden.
Herunterladen und in einen Ordner entpacken. Die Dateien haben keine grafische Oberfläche. Wir nutzen Sie auf der Kommandozeile.

Viele FFMPEG Parameter

Mit FFMPEG einen Screenshot aus einem Video erstellen

  ffmpeg -ss SEKUNDEN_START|HH:MM:SS -i PFAD_ZUM_VIDEO -t 0.001 -s WIDTHxHEIGHT pic.jpg

  • -ss Startpunkt im Film als Zeitangabe
    Entweder Sekunden oder eine Uhrzeit
  • -i Pfad zum Film selbst
  • -t Dauer der Aufnahme in Sekunden.
    Wir benötigen ja nur ein Bild, also die kleinste Dauer
  • -s Die Ausgabegrösse für das Bild in Pixel
    Breite mal Höhe
  • Der Bilddateiname muss eine bekannte Bildformat-Dateiendung haben!
    Am besten .jpg nehmen

Beispiel für einen Screen nach 20 Sekunden im Film

  ffmpeg -ss 20 -i meinFilm.mp4 -t 0.001 -s 320x240 pic.jpg

Mit FFMPEG einen kleines Vorschauvideo aus einem Video schneiden

  ffmpeg -ss SEKUNDEN_START|HH:MM:SS -i PFAD_ZUM_VIDEO -t DAUER -s WIDTHxHEIGHT pre.mp4

  • -ss Startpunkt im Film als Zeitangabe
    Entweder Sekunden oder eine Uhrzeit
  • -i Pfad zum Film selbst
  • -t Dauer der Aufnahme in Sekunden.
    Wir benötigen nur 20 Sekunden, also die kleinste Dauer
  • -s Die Ausgabegrösse für das Bild in Pixel

Beispiel für einen 20 Sekunden Schnitt nach 20 Sekunden im Film

  ffmpeg -ss 20 -i meinFilm.mp4 -t 20 -s 320x240 pre.mp4

Mit FFMPEG sich selbst über Webcam aufnehmen

Natürlich können wir auch Videos aufnehmen. Dazu nutzen wir unsere Webcam. Da jede Webcam anders benannt ist, suchen wir zuerst den Namen des Eingabegerätes heraus. Danach nutzen wir dieses.

Holen der angeschlossenen Eingabegeräte:

Video mit der Webcam aufnehmen für 10 Sekunden:

  ffmpeg -f dshow -i video="Integrated Camera" -t 10 output.flv

Foto (320x240 gross) mit der Webcam aufnehmen nach 10 Sekunden:

  ffmpeg -f dshow -i video="Integrated Camera" -ss 10 -s 320x240 -t 0.001 output.jpg

Buchtipps

Wenn man seinen eigenen Video-Server betreiben will, könnte dieses Wissen nützlich sein:

Fazit

Mit FFMPEG kann man schnell und unkompliziert Videos streamen, schneiden und umwandeln. Oft werden teuere kommerzielle Lösungen gar nicht benötigt. Gerade im Web kann man es mit den meisten Sprachen nutzen oder am besten gleich immer in einem eigenen Prozess auf der Konsole starten.

Mit etwas Geschick baut man sich schnell einen eigenen Videoserver. Siehe mein Youtube-Clone Post:
http://sasonikolov.blogspot.de/2013/03/youtube-clone-selber-gemacht-mit.html

Viel Spass
Saso Nikolov

Montag, 17. Juni 2013

CAS Single SignOn SSO verstehen

SingleSignOn SSO ist überall zu hören und lesen.
Die Implementierung wird durch viele Klassen und Bibliotheken abgebildet. Doch manchmal möchte man einfacher und simpler an die Sache heran gehen. Dazu ist das grundlegende Verständnis wichtig. Wie funktioniert das mit SSO und CAS?

Grundsätzlich wozu braucht man SSO?

Will man in seiner (Web)Anwendung oder Webseite keine eigene Userverwaltung haben, kann man dies an einen entsprechenden Server weiterverlagern. Der User muss nur einmal eine Registrierung durchführen und kann sich dann an viele Dienste anmelden, ohne dass seine Logindaten weiter gegeben werden. Dazu braucht er sich auch nur noch einen Zugang zu merken.

Die Anwendung leitet den Benutzer an den Authentifizierungsserver. Dort meldet sich der User an. Danach wird der User zurück zur Anwendung gesendet. Der Anwendungsserver fragt nun den Authentifizierungsserver ob die Anmeldung erfolgreich war und wer der Benutzer ist. In der Regel bekommt der Anwendungsserver dann nur eine eindeutige Kennung, damit er den Benutzer verwalten kann. Aber keine Daten, damit er sich an den Userdaten zu schaffen machen kann.

CAS Server bietet den Zugang zum SingleSignOn (SSO)

Der CAS-Server bietet:
  • Loginverfahren
  • Daten über den User für die Anwendungen und Programme
Benutzer sind hier hinterlegt. Dieser Dienst ist in der Regel von einer vertrauenswürdigen Seite. Meist kann der Benutzer hier auch entscheiden, welche Daten an Dritte (Anwendungen) freigegeben werden sollen. Bspw: email, kennung, Name, etc.

Anwendung

Der Benutzer möchte sich hier anmelden. Dabei aber nicht seine Daten hinterlegen. Die Anwendung erhält eine freigegeben Kennung vom CAS Server, wenn der Benutzer angemeldet ist.

Ablauf technisch

Benutzer geht auf die Anwendungs-Webseite. Diese prüft ob der Benutzer schon angemeldet ist. Dabei kann die Anwendung nur prüfen, ob der Benutzer bei der Anwendung angemeldet ist. Wenn der User nicht angemeldet scheint, sendet die Anwendung einen Redirect an den Browser des Benutzers. Darin ist die URL des CAS-Servers und eine URL, die aufgerufen werden soll, wenn der Benutzer angemeldet ist.

Der Benutzer loggt sich am CAS Server ein. Wird dann an die Anwendung zurück gesendet. Diese bekommt zusätzlich noch einen Parameter mit, der eine Ticket-Nummer vom CAS-Server enthält. Die Anwendung ruft nun Ihrerseits den CAS-Server auf und übergibt die Ticket-Nummer und erhält dann die Kennung des Benutzers.

Mit dieser Anwendung meldet nun die Anwendung den Benutzer bei sich selbst an.

Das alles geht sehr schnell und der Benutzer merkt in der Regel kaum eine Verzögerung.

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