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