2012-10-07 11 views
6

Voglio riprodurre un video in streaming sul mio IPad tramite il tag video HTML5 con tapestry5 (5.3.5) sul backend. Di solito il framework serveride non dovrebbe nemmeno giocare un ruolo in questo, ma in qualche modo lo fa.Lo streaming video su ipad non funziona con Tapestry5

In ogni caso, speriamo che qualcuno qui possa aiutarmi. Tieni presente che il mio progetto è un prototipo e che ciò che descrivo è semplificato/ridotto alle parti pertinenti. Lo apprezzerei molto se le persone non rispondessero con l'obbligo "si vuole fare la cosa sbagliata" o la sicurezza/prestazioni nitpicks che non sono rilevanti per il problema.

così qui va:

Setup

Ho un video preso da Apple HTML5 vetrina quindi so che il formato non è un problema. Ho una semplice pagina tml "Play" che contiene solo un tag "video".

Problema

ho iniziato mediante l'attuazione di un RequestFilter che gestisce la richiesta del controllo video aprendo il file video di riferimento e in streaming al cliente. È fondamentale "se il percorso inizia con" file ", quindi copia il file inputstream nella risposta outputstream". Funziona molto bene con Chrome ma non con l'Ipad. Bene, pensavo che dovessero esserci alcune intestazioni che mi mancano quindi ho di nuovo guardato l'Apple Showcase e ho incluso le stesse intestazioni e il tipo di contenuto ma nessuna gioia.

Successivamente, anche se, beh, vediamo cosa succede se lascio che t5 serva il file. Ho copiato il video nel contesto webapp, ho disattivato il filtro delle richieste e inserito il semplice nome file nell'attributo src del video. Funziona su Chrome AND IPad. Questo mi ha sorpreso e mi ha spinto a vedere come T5 gestisce i file statici/la richiesta di contesto. Finora sono arrivato così lontano da sentire che ci sono due percorsi diversi che ho confermato spostando il "video src" cablato in un asset con un @Path ("context:"). Questo, ancora, funziona su Chrome ma non su IPad.

Quindi sono davvero perso qui. Qual è questo succo segreto nelle richieste di "contesto semplice" che gli consentono di lavorare su IPad? Non c'è niente di speciale in corso e tuttavia è l'unico modo in cui funziona. Il problema è, non posso servire realmente quei vids dal mio contesto webapp ...

Soluzione

Così, si scopre che c'è questa intestazione http chiamato "Gamma" e che l'iPad, a differenza di Chrome utilizza con il video. La "salsa segreta" è quindi che il gestore di servlet per la richiesta di risorse statiche sa come gestire le richieste di intervallo mentre quelle di T5 no. Ecco la mia implementazione personalizzata:

 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(); 
     } 

risposta

7

Voglio postare la mia soluzione raffinata dall'alto. Spero che questo sia utile a qualcuno.

Quindi il problema sembrava essere che ignoravo l'intestazione della richiesta http "Range" a cui l'IPad non piaceva.In poche parole questa intestazione significa che il client desidera solo una determinata parte (in questo caso un intervallo di byte) della risposta.

Questo è ciò che una richiesta di video HTML iPad assomiglia ::

[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 

Ciò significa che l'iPad vuole solo il primo byte. Se non si tiene conto di questa intestazione e si invia semplicemente una risposta di 200 con il corpo completo, il video non verrà riprodotto. Quindi, è necessario inviare un 206 risposta (risposta parziale) e impostare le seguenti intestazioni di risposta:

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

Questo vuol dire "Io ti mando BYTE 0 a 1 su 357.772.702 byte totali disponibili".

Quando si effettivamente iniziare la riproduzione del video, la richiesta successiva sarà simile a questa (tutto tranne l'intestazione gamma ommited):

[INFO] RequestLogger Range:bytes=0-357772701 

Quindi la mia soluzione raffinata assomiglia a questo:

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(); 

     } 

Questa soluzione è migliore della mia prima perché gestisce tutti i casi per richieste "Range" che sembra essere un prerequisito per i clienti come Chrome che possono supportare il salto all'interno del video (a quel punto emetteranno una richiesta di intervallo per quello punto nel video).

Tuttavia non è ancora perfetto. Ulteriori miglioramenti consisterebbero nell'impostare correttamente l'intestazione "Last-Modified" e fare una corretta gestione dei client richiede un intervallo non valido o un intervallo di qualcos'altro rispetto ai byte.

+0

Questa è un'informazione utile; non c'è motivo per cui Tapestry non possa gestirlo automaticamente all'interno del codice standard di gestione degli asset; non ci rendiamo conto che è necessario. L'aggiunta di questo livello di informazioni al nostro JIRA è il primo passo. –

+0

Ottima risposta. Funziona come un fascino subito. Molte grazie. –

0

Sospetto che si tratti più dell'iPad che di Tapestry.

Potrei invocare Response.disableCompression() prima di scrivere il flusso nella risposta; Tapestry potrebbe provare a utilizzare GZIP per il tuo stream e l'iPad potrebbe non essere preparato per questo, poiché i formati di video e immagini sono solitamente già compressi.

Inoltre, non viene visualizzata l'intestazione del tipo di contenuto; ancora una volta l'iPad potrebbe semplicemente essere più sensibile a quello di Chrome.

+0

Ciao Howard. Penso che sia bello che tu abbia il tempo di rispondere a T5 (un ottimo framework) qui su Stackoverflow. Ad ogni modo, ho scoperto quale fosse il problema e ho aggiunto la soluzione alla mia domanda. La versione TL; DR è che l'iPad non gli piace se non si tiene conto dell'intestazione della richiesta http "Range". Questo potrebbe essere un problema per T5 anche perché da quello che dico, quando il framework sta servendo un asset, ignorerà anche l'intestazione Range. Pubblicherò una risposta con maggiori dettagli. – Wulf