2012-10-07 6 views
6

Ich möchte ein Video zu meinem IPad über das HTML5-Video-Tag mit tapestry5 (5.3.5) auf dem Back-End streamen. Normalerweise sollte das serverseitige Framework nicht einmal eine Rolle spielen, aber irgendwie tut es das auch.Video-Streaming zu iPad funktioniert nicht mit Tapestry5

Wie auch immer, hoffentlich kann mir hier jemand helfen. Bitte bedenken Sie, dass mein Projekt ein Prototyp ist und dass das, was ich beschreibe, auf die relevanten Teile reduziert ist. Ich würde es sehr schätzen, wenn die Leute nicht mit dem obligatorischen "Sie wollen das Falsche tun" oder Sicherheits-/Performance-Nitpicks antworten, die für das Problem nicht relevant sind.

Also hier geht es:

Setup-

Ich habe ein Video aus dem Apple HTML5 genommen präsentieren, damit ich weiß, dass Format kein Problem ist. Ich habe eine einfache Tml-Seite "Play", die nur ein "Video" -Tag enthält.

Problem

Ich begann mit einem RequestFilter Implementierung, die die Anforderung von den Videosteuergriffen durch die referenzierte Videodatei zu öffnen und es zu Client-Streaming. Das ist grundlegend "wenn der Pfad mit 'Datei' beginnt, dann kopiere den Datei-Eingangsstrom in den Antwort-Ausgangsstrom". Dies funktioniert sehr gut mit Chrome, aber nicht mit dem Ipad. Gut, aber ich muss einige Header haben, die ich vermisse, also habe ich das Apple Showcase erneut angeschaut und die gleichen Header und Inhaltstypen, aber keine Freude.

Als nächstes, ich, naja, mal sehen, was passiert, wenn ich T5 die Datei liefern lassen. Ich habe das Video in den Webapp-Kontext kopiert, meinen Anforderungsfilter deaktiviert und den einfachen Dateinamen in das src-Attribut des Videos eingefügt. Dies funktioniert in Chrome UND IPad. Das überraschte mich und veranlasste mich zu sehen, wie T5 statische Dateien/Kontextanforderung behandelt. Bislang habe ich nur das Gefühl, dass es zwei verschiedene Wege gibt, die ich durch den Wechsel des festverdrahteten "video src" zu einem Asset mit einem @Path ("context:") bestätigt habe. Dies funktioniert wiederum in Chrome, nicht jedoch in IPad.

Also ich bin wirklich hier verloren. Was ist dieser geheime Saft in den "einfachen Kontext" -Anfragen, die es erlauben, auf dem IPad zu arbeiten? Es gibt nichts Besonderes und doch funktioniert es nur so. Das Problem ist, ich kann nicht wirklich diese vids von meinem Webapp Kontext dienen ...

Lösung

Also, es stellt sich heraus, dass es diese HTTP-Header „Range“ und dass das iPad, im Gegensatz zu Chrome verwendet es mit Video. Die "geheime Soße" besteht dann darin, dass der Servlet-Handler für die statische Ressourcenanforderung weiß, wie er mit Bereichsanforderungen umgehen soll, während T5 dies nicht tut. Hier ist meine benutzerdefinierte Implementierung:

 OutputStream os = response.getOutputStream("video/mp4"); 
     InputStream is = new BufferedInputStream(new FileInputStream(f)); 
     try { 
      String range = request.getHeader("Range"); 
      if(range != null && !range.equals("bytes=0-")) { 
       logger.info("Range response _______________________"); 
       String[] ranges = range.split("=")[1].split("-"); 
       int from = Integer.parseInt(ranges[0]); 
       int to = Integer.parseInt(ranges[1]); 
       int len = to - from + 1 ; 

       response.setStatus(206); 
       response.setHeader("Accept-Ranges", "bytes"); 
       String responseRange = String.format("bytes %d-%d/%d", from, to, f.length()); 
       logger.info("Content-Range:" + responseRange); 
       response.setHeader("Connection", "close"); 
       response.setHeader("Content-Range", responseRange); 
       response.setDateHeader("Last-Modified", new Date().getTime()); 
       response.setContentLength(len); 
       logger.info("length:" + len); 

       byte[] buf = new byte[4096]; 
       is.skip(from); 
       while(len != 0) { 

        int read = is.read(buf, 0, len >= buf.length ? buf.length : len); 
        if(read != -1) { 
         os.write(buf, 0, read); 
         len -= read; 
        } 
       } 


      } else { 
        response.setStatus(200); 
        IOUtils.copy(is, os); 
      } 

     } finally { 
      os.close(); 
      is.close(); 
     } 

Antwort

7

Ich möchte meine raffinierte Lösung von oben veröffentlichen. Hoffentlich wird das für jemanden nützlich sein.

Also im Grunde schien das Problem zu sein, dass ich den HTTP-Request-Header "Range" ignorierte, den das IPad nicht mochte.Zusammenfassend bedeutet dieser Header, dass der Client nur einen bestimmten Teil (in diesem Fall einen Bytebereich) der Antwort benötigt.

Dies ist, was eine iPad html Video Anfrage sieht aus wie ::

[INFO] RequestLogger Accept:*/* 
[INFO] RequestLogger Accept-Encoding:identity 
[INFO] RequestLogger Connection:keep-alive 
[INFO] RequestLogger Host:mars:8080 
[INFO] RequestLogger If-Modified-Since:Wed, 10 Oct 2012 22:27:38 GMT 
[INFO] RequestLogger Range:bytes=0-1 
[INFO] RequestLogger User-Agent:AppleCoreMedia/1.0.0.9B176 (iPad; U; CPU OS 5_1 like Mac OS X; en_us) 
[INFO] RequestLogger X-Playback-Session-Id:BC3B397D-D57D-411F-B596-931F5AD9879F 

Es bedeutet, dass das iPad nur das erste Byte will. Wenn Sie diesen Header ignorieren und einfach eine 200-Antwort mit dem vollständigen Text senden, wird das Video nicht abgespielt. So müssen Sie eine 206-Antwort (Teilantwort) senden und folgende Antwort-Header:

[INFO] RequestLogger Content-Range:bytes 0-1/357772702 
[INFO] RequestLogger Content-Length:2 

Das bedeutet, „Ich schicke Ihnen 0 bis 1 von 357.772.702 insgesamt verfügbaren Bytes Byte“.

Wenn Sie tatsächlich starten Sie das Video abgespielt wird, die nächste Anforderung wie diese (alles außer dem Bereich Kopf ommited) aussehen:

[INFO] RequestLogger Range:bytes=0-357772701 

So sieht meine raffinierte Lösung wie folgt aus:

OutputStream os = response.getOutputStream("video/mp4"); 

     try { 
       String range = request.getHeader("Range"); 
       /** if there is no range requested we will just send everything **/ 
       if(range == null) { 
        InputStream is = new BufferedInputStream(new FileInputStream(f)); 
        try { 
         IOUtils.copy(is, os); 
         response.setStatus(200); 
        } finally { 
         is.close(); 
        } 
        return true; 
       } 
       requestLogger.info("Range response _______________________"); 


       String[] ranges = range.split("=")[1].split("-"); 
       int from = Integer.parseInt(ranges[0]); 
       /** 
       * some clients, like chrome will send a range header but won't actually specify the upper bound. 
       * For them we want to send out our large video in chunks. 
       */ 
       int to = HTTP_DEFAULT_CHUNK_SIZE + from; 
       if(to >= f.length()) { 
        to = (int) (f.length() - 1); 
       } 
       if(ranges.length == 2) { 
        to = Integer.parseInt(ranges[1]); 
       } 
       int len = to - from + 1 ; 

       response.setStatus(206); 
       response.setHeader("Accept-Ranges", "bytes"); 
       String responseRange = String.format("bytes %d-%d/%d", from, to, f.length()); 

       response.setHeader("Content-Range", responseRange); 
       response.setDateHeader("Last-Modified", new Date().getTime()); 
       response.setContentLength(len); 

       requestLogger.info("Content-Range:" + responseRange); 
       requestLogger.info("length:" + len); 
       long start = System.currentTimeMillis(); 
       RandomAccessFile raf = new RandomAccessFile(f, "r"); 
       raf.seek(from); 
       byte[] buf = new byte[IO_BUFFER_SIZE]; 
       try { 
        while(len != 0) { 
         int read = raf.read(buf, 0, buf.length > len ? len : buf.length); 
         os.write(buf, 0, read); 
         len -= read; 
        } 
       } finally { 
        raf.close(); 
       } 
       logger.info("r/w took:" + (System.currentTimeMillis() - start)); 




     } finally { 
      os.close(); 

     } 

Diese Lösung ist besser als meine erste, da sie alle Fälle für "Range" -Aufforderungen behandelt, was für Kunden wie Chrome eine Voraussetzung ist, um das Überspringen innerhalb des Videos zu unterstützen (an diesem Punkt wird eine Bereichsanfrage dafür ausgegeben) Punkt im Video).

Es ist immer noch nicht perfekt. Weitere Verbesserungen würden den Header "Last-Modified" korrekt setzen und die korrekte Behandlung von Clients würde einen ungültigen Bereich oder einen Bereich von etwas anderem als Bytes erfordern.

+0

Dies sind nützliche Informationen; Es gibt keinen Grund, warum Tapestry dies nicht automatisch im Standard-Asset-Handling-Code handhaben kann. Wir sind uns nur nicht bewusst, dass es getan werden muss. Das Hinzufügen dieses Informationslevels zu unserer JIRA ist der erste Schritt. –

+0

Ausgezeichnete Antwort. Funktioniert sofort wie ein Zauber. Danke vielmals. –

0

Ich vermute, das ist mehr über das iPad als über Tapestry.

Ich könnte Response.disableCompression() vor dem Schreiben des Streams auf die Antwort aufrufen; Tapestry versucht möglicherweise, Ihren Stream mit GZIP zu versehen, und das iPad ist möglicherweise nicht darauf vorbereitet, da Video- und Bildformate normalerweise bereits komprimiert sind.

Auch ich sehe keinen Content-Type-Header gesetzt; Auch hier ist das iPad vielleicht sensibler als Chrome.

+0

Hallo Howard. Ich finde es toll, dass Sie sich die Zeit nehmen, T5 (ein tolles Framework) hier auf Stackoverflow zu beantworten. Wie auch immer, ich habe herausgefunden, was das Problem war und habe die Lösung zu meiner Frage hinzugefügt. Die TL; DR-Version ist, dass das iPad es nicht mag, wenn Sie den HTTP-Anfrage-Header "Range" ignorieren. Dies könnte ein Problem für T5 sein, denn von dem, was ich sage, wenn das Framework ein Asset bedient, ignoriert es auch den Range-Header. Ich werde eine Antwort mit mehr Details veröffentlichen. – Wulf

Verwandte Themen