2012-10-07 55 views
6

我想通過後端帶有tapestry5(5.3.5)的HTML5視頻標籤將視頻流式傳輸到我的iPad。通常服務器端框架甚至不應該在這方面發揮作用,但它不知道如何。視頻流式傳輸到ipad不適用於Tapestry5

無論如何,希望這裏有人能幫助我。請記住,我的項目是一個原型,我所描述的內容被簡化/簡化爲相關部分。如果人們沒有迴應強制性的「你想做錯事」或與問題無關的安全/性能問題,我將非常感激。

所以這裏有雲:

設置

我從蘋果HTML5拍攝的視頻展示,所以我知道該格式是不是一個問題。我有一個簡單的tml頁面「播放」,只包含一個「視頻」標籤。

問題

我開始通過實施處理通過打開引用的視頻文件,並將其流媒體客戶端從視頻控制的請求RequestFilter。這是基本的「如果路徑以'文件'開始,然後將文件輸入流複製到響應輸出流」。這對Chrome非常適用,但不適用於Ipad。好吧,我雖然,一定是我失蹤的一些標題,所以我再次看着Apple Showcase,幷包含相同的標題和內容類型,但沒有喜悅。

接下來,我想,讓我們看看如果我讓t5服務文件會發生什麼。我將視頻複製到了webapp上下文,禁用了我的請求過濾器,並將簡單文件名放在視頻的src屬性中。這適用於Chrome和iPad。 這讓我感到驚訝,並促使我看看T5如何處理靜態文件/上下文請求。到目前爲止,我只是覺得有兩種不同的方式,我已經通過使用@Path(「context:」)將硬連線「視頻src」切換到資產來確認。這又一次適用於Chrome,但不適用於iPad。

所以我真的迷失在這裏。在「簡單的上下文」請求中,這個祕密果汁是什麼使它能夠在IPad上工作?沒有什麼特別的,但它是唯一的方法。問題是,我真的不能從我的web應用程序上下文服務於那些西元...

解決方案

所以,事實證明,有這個所謂的「範圍」 HTTP頭和iPad的,不同於Chrome使用它與視頻。那麼「祕訣」就是靜態資源請求的servlet處理程序知道如何處理範圍請求,而T5則不知道。這是我的自定義實現:

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

回答

7

我想從上面發佈我的精製解決方案。希望這會對某人有用。

所以基本上這個問題似乎是我無視IPad不喜歡的「Range」http請求頭。簡而言之,此表頭意味着客戶端只需要響應的某個部分(在本例中爲字節範圍)。

這是一個iPad HTML視頻請求是什麼樣子::

[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 

這意味着iPad的唯一希望第一個字節。如果你不理會這個標題,只發送一個200全身響應,那麼視頻就不會播放。所以,你需要發送一個響應206(部分響應),並設置以下響應頭:

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

這意味着「我送你通過357772702 1可用的總字節數字節0」。

當你真正開始播放視頻,下一個請求看起來像這樣(除範圍標頭的一切中省略):

[INFO] RequestLogger Range:bytes=0-357772701 

所以我精解是這樣的:

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

     } 

這個解決方案比我的第一個解決方案更好,因爲它處理所有「範圍」請求的情況,這似乎是像Chrome這樣的客戶端的先決條件,能夠支持在視頻內跳過(此時,他們將發出範圍請求點在視頻中)。

雖然它還不完美。進一步的改進是正確設置「Last-Modified」標題,並且正確地處理客戶端請求無效範圍或其他字節的範圍。

+0

這是有用的信息;沒有理由爲什麼Tapestry無法在標準資產處理代碼中自動處理;我們只是不知道它需要做什麼。將這一級別的信息添加到我們的JIRA是第一步。 –

+0

優秀的答案。馬上就像魅力一樣工作。非常感謝。 –

0

我懷疑這是更多關於iPad比關於Tapestry。

在將流寫入響應之前,我可能會調用Response.disableCompression()掛毯可能會嘗試GZIP你的流,而iPad可能沒有爲此做好準備,因爲視頻和圖像格式通常已經被壓縮。

另外,我沒有看到正在設置的內容類型標題;再次,iPad可能會比Chrome更敏感。

+0

嗨,霍華德。我認爲你花時間在Stackoverflow上回答T5(一個很好的框架)是非常棒的。無論如何,我發現問題是什麼,並將解決方案添加到我的問題中。 TL; DR版本是iPad不喜歡它,如果你忽略「範圍」http請求標頭。這可能是T5的一個問題,也是因爲從我說的話來看,當框架提供資源時,它也會忽略Range頭部。我會發佈一個更詳細的答案。 – Wulf