質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
JSP

JSP(Java Server Pages)とは、ウェブアプリケーションの表示レイヤーに使われるサーバーサイドの技術のことです。

Java

Javaは、1995年にサン・マイクロシステムズが開発したプログラミング言語です。表記法はC言語に似ていますが、既存のプログラミング言語の短所を踏まえていちから設計されており、最初からオブジェクト指向性を備えてデザインされています。セキュリティ面が強力であることや、ネットワーク環境での利用に向いていることが特徴です。Javaで作られたソフトウェアは基本的にいかなるプラットフォームでも作動します。

Struts 2

Apache Struts 2は、Apache Strutsプロジェクトにて開発されているオープンソースのJavaベースのWebアプリケーションフレームワークです。Sturts1に比べ、設定ファイルの削減、依存性の注入、POJO等の改善がなされています。

Q&A

2回答

1320閲覧

Struts2で取得した動画がスマホで再生されない

Begi

総合スコア56

JSP

JSP(Java Server Pages)とは、ウェブアプリケーションの表示レイヤーに使われるサーバーサイドの技術のことです。

Java

Javaは、1995年にサン・マイクロシステムズが開発したプログラミング言語です。表記法はC言語に似ていますが、既存のプログラミング言語の短所を踏まえていちから設計されており、最初からオブジェクト指向性を備えてデザインされています。セキュリティ面が強力であることや、ネットワーク環境での利用に向いていることが特徴です。Javaで作られたソフトウェアは基本的にいかなるプラットフォームでも作動します。

Struts 2

Apache Struts 2は、Apache Strutsプロジェクトにて開発されているオープンソースのJavaベースのWebアプリケーションフレームワークです。Sturts1に比べ、設定ファイルの削減、依存性の注入、POJO等の改善がなされています。

2グッド

3クリップ

投稿2017/08/28 07:46

編集2017/09/05 06:55

###前提・実現したいこと
Struts2で動画をサーバから取得し、動的にvideoタグを生成してそこにソースを当て込み動画を再生させています。
PCからは問題なく閲覧できていますが、スマートフォンやタブレットでは閲覧できない場合があるため、これを解決させたいです。現在iphone、Androidともに再生できていません。
https://teratail.com/questions/85991
の質問とほぼ同じなのですが、問題の箇所が絞り込めたため再度投稿させていただきました。
動画ファイルはmp4でH.264形式です。ファイルサイズは約4MBです。
動画ファイルのパスを隠すため、JSPではActionクラスを呼び、そこでパスを探しています。

###発生している問題・エラーメッセージ
iphone5Sで動画を再生させようとすると、https://teratail.com/questions/85991の現象が発生します。
また、サーバ側でリクエスト後に以下のメッセージが表示されます。
重大: Exception occurred during processing request: java.io.IOException: 確立された接続がホスト コンピューターのソウトウェアによって中止されました。 [月 8 28 16:19:18 JST 2017]

###該当のソースコード
videoタグのsrcで指定するActionクラスです。

VideoAction.java

1package action; 2import 省略; 3@Namespace("/") 4@ParentPackage("test") 5@Results({ 6 @Result(name = "video", type = "stream", 7 params = { 8 "inputName", "inputStream", 9 "contentType", "video/mp4", 10 "contentLength", "${contentLength}", 11 "contentDisposition", "attachment; filename = ${fileName}" 12 }), 13 @Result( name="error" , type="dispatcher", location="/error.jsp") 14}) 15public class VideoAction extends ActionSupport { 16 private InputStream inputStream; 17 private long contentLength; 18 private String fileName; 19 private String lotNo; 20 private String reqFilePath; 21 22 public String execute() { 23 String lotDir = "/test/"; 24 BufferedInputStream bis = null; 25 26 try { 27 File lotNoFile = new File(lotDir + this.lotNo); 28 bis = new BufferedInputStream(new FileInputStream(lotNoFile)); 29 30 byte[] byteArray = IOUtils.toByteArray(bis); 31 this.inputStream = new ByteArrayInputStream(byteArray); 32 this.contentLength = byteArray.length; 33 34 return "video"; 35 } catch (Exception e) { 36 return "error"; 37 } finally { 38 try { 39 bis.close(); 40 } catch (IOException e) { 41 e.printStackTrace(); 42 } 43 44 } 45 } 46 47 public InputStream getInputStream() { 48 return inputStream; 49 } 50 public void setInputStream(InputStream inputStream) { 51 this.inputStream = inputStream; 52 } 53 public long getContentLength() { 54 return contentLength; 55 } 56 public void setContentLength(long contentLength) { 57 this.contentLength = contentLength; 58 } 59 public String getFileName() { 60 return fileName; 61 } 62 public void setFileName(String fileName) { 63 this.fileName = fileName; 64 } 65 public String getLotNo() { 66 return lotNo; 67 } 68 public void setLotNo(String lotNo) { 69 this.lotNo = lotNo; 70 } 71 public String getFilePath() { 72 return reqFilePath; 73 } 74 public void setFilePath(String filePath) { 75 this.reqFilePath = filePath; 76 } 77}

video.jsp

1<video src='video.action?lotNo=1></video>

###試したこと
動画ファイルに問題があるかと思い調査しましたが、プロジェクトのルートディレクトリ以下に直接mp4ファイルを配置し、ソースにパスを直書きしたらiphoneで動画が再生できたため、動画の問題ではないと思います。

###補足情報(言語/FW/ツール等のバージョンなど)
Struts2.3.24

VideoAction.javaのResult又はStreamの指定に問題があるのではと思っています。
情報に不足などありましたらご指摘ください。よろしくお願いします。

###追記
othersight様、A-pZ様
時間が空いてしまい申し訳ありません。
https://www.stevesouders.com/blog/2013/04/21/html5-video-bytes-on-ios/
等を参考に、レスポンスヘッダにAccept-Range、Content-Rangeを仕込み、Content-LengthはリクエストヘッダのRangeが0-1だった場合は2を返すなど行いましたが、未だ解決に至っていません。
ソースは以下になります。

VideoAction.java

1HttpServletRequest request = getRequest(); //リクエストヘッダを取得 2String range = request.getHeader("Range").replace("bytes=", ""); 3String rangeFirst = range.substring(0, range.indexOf("-")); 4String rangeLast = range.substring(range.indexOf("-") + 1, range.length()); 5String contentRange = ""; 6 7HttpServletResponse response = getResponse(); 8response.setStatus(206); 9response.setHeader("Accept-Ranges", "bytes"); 10 11//range: "0-" 12if (range.equals("0-")) { 13contentRange = "bytes " + rangeFirst + "-" + (Integer.parseInt(rangeFirst) + 1) + "/" + this.contentLength; 14} 15//range: "0-x" 16if (range.indexOf("0-") != -1 && (!rangeLast.isEmpty() && !rangeLast.equals("0"))) { 17contentRange = "bytes " + rangeFirst + "-" + rangeLast + "/" + this.contentLength; 18} 19//range: "x-" 20if (!rangeFirst.equals("0") && range.lastIndexOf("-") != -1) { 21contentRange = "bytes " + rangeFirst + "-" + String.valueOf(this.contentLength - 1) + "/" + this.contentLength; 22} 23//range: "0-1" 24if (range.equals("0-1")) { 25contentRange = "bytes 0-1/" + this.contentLength; 26this.contentLength = 2; 27} 28//range: "x-x" 29if (!rangeFirst.equals("0") && range.lastIndexOf("-") == -1) { 30contentRange = "bytes " + rangeFirst + "-" + (Integer.parseInt(rangeLast) + 1) + "/" + this.contentLength; 31this.contentLength = 2; 32} 33 34response.setHeader("Content-Range", contentRange); 35System.out.println("★レスポンスRange--- " + contentRange); 36System.out.println("★コンテントLength--- " + this.contentLength); 37

上記のソースを仕込み、1回目のリクエストに対しては正常に動作しましたが2回目のリクエストからはサーバ側で
重大: Exception occurred during processing request: java.io.IOException: 確立された接続がホスト コンピューターのソウトウェアによって中止されました。 [月 8 28 16:19:18 JST 2017]
がまた発生してしまいます。
Last-ModifiedやEtagなど仕込むことも試しましたが、変わらないようです。
以下リクエスト、レスポンスになります。

1回目のリクエスト Accept:*/* Accept-Encoding:identity;q=1, *;q=0 Accept-Language:ja,en-US;q=0.8,en;q=0.6 Cache-Control:no-cache Connection:keep-alive Cookie:JSESSIONID=0D946AEE3ABF37850218436884DEB391; _ga=GA1.1.2024693826.1504524761; _gid=GA1.1.353248483.1504524761 Host:localhost:8080 Pragma:no-cache Range:bytes=0- Referer:http://localhost:8080/test/test.action User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36
1回目のレスポンス Accept-Ranges:bytes Connection:Keep-Alive Content-Disposition:attachment; filename = Content-Language:ja Content-Length:1595162 Content-Range:bytes 0-1/1595162 Content-Type:video/mp4 Date:Tue, 05 Sep 2017 06:35:37 GMT Etag:42b795-4867d5fcac1c0 Server:Apache-Coyote/1.1
2回目のリクエスト Accept:*/* Accept-Encoding:identity;q=1, *;q=0 Accept-Language:ja,en-US;q=0.8,en;q=0.6 Cache-Control:no-cache Connection:keep-alive Cookie:JSESSIONID=0D946AEE3ABF37850218436884DEB391; _ga=GA1.1.2024693826.1504524761; _gid=GA1.1.353248483.1504524761 Host:localhost:8080 Pragma:no-cache Range:bytes=1572864- Referer:http://localhost:8080/test/test.action User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36
2回目のレスポンス Accept-Ranges:bytes Connection:Keep-Alive Content-Disposition:attachment; filename = Content-Language:ja Content-Length:1595162 Content-Range:bytes 1572864-1595161/1595162 Content-Type:video/mp4 Date:Tue, 05 Sep 2017 06:35:37 GMT Etag:42b795-4867d5fcac1c0 Server:Apache-Coyote/1.1

すみませんが、何かわかることがありましたらご教授いただけますでしょうか。
よろしくお願いいたします。

A-pZ, ikuwow👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

ファイル分割に対応せず、Rangeヘッダに対応するのであれば、以下で動画の再生はできるようになります。いくつかポイントがあります。

struts.xmlにて、Struts2のStreamResultを継承したResultを新設する。ここではMovieResultと名付けて、以下のように設定する。

xml

1<struts> 2 <package name="sample" abstract="true" extends="struts-default"> 3 <result-types> 4 <result-type name="movie" class="lumi.sample.result.MovieResult" /> 5 </result-types> 6(以下省略)

これでResultのtypeにmovieを指定すると、MovieResultへ処理が移動します。

続いてActionクラスは、Struts2標準のダウンロードのしきたり通りの情報を書き込みます。以下の例はStruts2.5系ですが、2.3系でも動作します。

java

1import java.io.File; 2import java.io.InputStream; 3import java.net.URL; 4 5import org.apache.struts2.convention.annotation.Action; 6import org.apache.struts2.convention.annotation.Namespace; 7import org.apache.struts2.convention.annotation.ParentPackage; 8import org.apache.struts2.convention.annotation.Result; 9import org.apache.struts2.convention.annotation.Results; 10import org.springframework.context.annotation.Scope; 11import org.springframework.stereotype.Controller; 12 13import com.opensymphony.xwork2.ActionSupport; 14 15import lombok.Getter; 16import lombok.extern.log4j.Log4j2; 17import lumi.action.LumiActionSupport; 18 19/** 20 * Actionクラスのテンプレートサンプル。 21 * 22 * @author A-pZ ( Serendipity 3 ./ as sundome goes by. ) 23 */ 24@Namespace("/") 25@ParentPackage("sample") 26@Results({ 27 // location属性に指定したhtmlファイル名は、/WEB-INF/content 以下からの相対パス。 28 @Result(name = ActionSupport.SUCCESS, type = "movie", params = { "contentType", 29 "video/mp4", "inputName","stream", "contentDisposition", 30 "filename=\"${filename}\"","contentLength","${length}"}),}) 31public class DownloadAction extends LumiActionSupport { 32 33 @Action("download") 34 public String start() throws Exception { 35 36 filename = "waterfall-free-video1.mp4"; 37 stream = servletRequest.getServletContext().getResourceAsStream("/WEB-INF/waterfall-free-video1.mp4"); 38 URL url = servletRequest.getServletContext().getResource("/WEB-INF/waterfall-free-video1.mp4"); 39 File file = new File(url.getFile()); 40 length = file.length(); 41 return SUCCESS; 42 } 43 44 @Getter 45 private String filename; 46 @Getter 47 private InputStream stream; 48 @Getter 49 private Long length; 50 51}

※一部、Project lombokのアノテーションを利用してgetメソッドを省略しています。

最後にMovieResultの実装部分で、リクエストヘッダを判断して、レスポンスヘッダに追加情報を足します。

java

1import java.io.InputStream; 2import java.io.OutputStream; 3 4import javax.servlet.http.HttpServletRequest; 5import javax.servlet.http.HttpServletResponse; 6 7import org.apache.struts2.result.StreamResult; 8 9import com.opensymphony.xwork2.ActionInvocation; 10 11import lombok.Data; 12 13/** 14 * Range対応暫定(分割しない)のResult。これをひな形にして分割ストリーム処理もいけるはず。 15 * 16 * @author A-pZ 17 * 18 */ 19@Data 20public class MovieResult extends StreamResult { 21 protected void doExecute(String finalLocation, ActionInvocation invocation) throws Exception { 22 LOG.debug("Find the Response in context"); 23 24 HttpServletRequest oRequest = (HttpServletRequest) invocation.getInvocationContext().get(HTTP_REQUEST); 25 HttpServletResponse oResponse = (HttpServletResponse) invocation.getInvocationContext().get(HTTP_RESPONSE); 26 27 OutputStream oOutput = null; 28 29 try { 30 if (inputStream == null) { 31 inputStream = (InputStream) invocation.getStack().findValue(conditionalParse(inputName, invocation)); 32 } 33 34 LOG.debug("Set the content type: {};charset{}", contentType, contentCharSet); 35 if (contentCharSet != null && !contentCharSet.equals("")) { 36 oResponse.setContentType(conditionalParse(contentType, invocation) + ";charset=" 37 + conditionalParse(contentCharSet, invocation)); 38 } else { 39 oResponse.setContentType(conditionalParse(contentType, invocation)); 40 } 41 42 LOG.debug("Set the content length: {}", contentLength); 43 if (contentLength != null) { 44 String _contentLength = conditionalParse(contentLength, invocation); 45 int _contentLengthAsInt; 46 try { 47 _contentLengthAsInt = Integer.parseInt(_contentLength); 48 if (_contentLengthAsInt >= 0) { 49 oResponse.setContentLength(_contentLengthAsInt); 50 } 51 } catch (NumberFormatException e) { 52 LOG.warn("failed to recognize {} as a number, contentLength header will not be set", _contentLength, 53 e); 54 } 55 } 56 57 LOG.debug("Set the content-disposition: {}", contentDisposition); 58 if (contentDisposition != null) { 59 oResponse.addHeader("Content-Disposition", conditionalParse(contentDisposition, invocation)); 60 } 61 62 // リクエストヘッダにRangeがあった場合はストリーム再生用のレスポンスヘッダを追加 63 if (oRequest.getHeader("Range") != null) { 64 int start = 0; 65 String lengthString = conditionalParse(contentLength, invocation); 66 int end = Integer.parseInt(lengthString) - 1; 67 int length = Integer.parseInt(lengthString); 68 String contentRange = String.format("bytes %s-%s/%s", start, end, length); 69 oResponse.setHeader("Content-Range", contentRange); 70 oResponse.setHeader("Content-Length", contentLength); 71 oResponse.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); 72 } 73 74 oOutput = oResponse.getOutputStream(); 75 76 LOG.debug("Streaming result [{}] type=[{}] length=[{}] content-disposition=[{}] charset=[{}]", inputName, 77 contentType, contentLength, contentDisposition, contentCharSet); 78 79 LOG.debug("Streaming to output buffer +++ START +++"); 80 byte[] oBuff = new byte[bufferSize]; 81 int iSize; 82 while (-1 != (iSize = inputStream.read(oBuff))) { 83 LOG.debug("Sending stream ... {}", iSize); 84 oOutput.write(oBuff, 0, iSize); 85 } 86 87 LOG.debug("Streaming to output buffer +++ END +++"); 88 89 // Flush 90 oOutput.flush(); 91 } finally { 92 if (inputStream != null) { 93 inputStream.close(); 94 } 95 if (oOutput != null) { 96 oOutput.close(); 97 } 98 } 99 } 100 101}

検証環境がすぐに用意できないので、iOS/Androidで検証はしていませんが、PCブラウザでの動作はできています。

…しかし今時なら対応できそうな機能なので、拡張のresultとして公式にあっても良いのではないかと思いますね。

お返事が遅れて申し訳ありませでした(´・ω・`)

投稿2017/09/05 16:27

A-pZ

総合スコア12011

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

Struts2に関する知識がないためサーバ側の解決法まで紹介できないのが残念ですが、ブラウザ側の動作についてはご説明できればと思います。

モバイルブラウザでは、<video>要素で動画を再生する際、不要なダウンロードを少しでも削減するために、HTTPでいきなり先頭から末尾まで一度のHTTPリクエストでファイルをダウンロードするのではなく、ある程度の長さに区切って、少しずつダウンロードを試みるようになっています。

具体的には、ブラウザから送られるHTTPリクエストヘッダで、

http

1Range: bytes=0-213183

のような形で、ダウンロードするファイル内の位置が指定されます。これに対し、サーバ側では、HTTPレスポンスヘッダで、

http

1HTTP/1.1 206 Partial Content 2Content-Range: bytes 0-213183/51919826 3Content-Length: 213184

のような形でファイルの一部分であることを示してブラウザに渡すようにする必要があります。

発生したエラーがダウンロード中断のような現象を表しているのは、ブラウザが部分ダウンロードを要求したにも関わらず、サーバ側がファイル全体を渡そうとして、ブラウザが要求したサイズを送信完了しても送信が終了しなかったためと考えられます。

Strut2でのHTTPヘッダの扱い方がわからないため、説明できるのはここまでになってしまいますが、サーバ側でPartial Contentのダウンロードに対応していないとモバイルブラウザの<video>要素では正常に再生できない点には注意が必要です。

参考:
Safari Web Content Guide
MDN Web Docs: HTTP range requests

投稿2017/08/28 08:44

othersight

総合スコア356

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

A-pZ

2017/08/28 22:18

まさに othersight様のおっしゃるとおりで、HTTPヘッダでRangeを扱わなければなりません。 ですが標準のStruts2ではこのRangeヘッダに関するStreamResultの値は対応していませんので、拡張が必要です。
othersight

2017/08/29 03:30

コメントと補足ありがとうございます!
Begi

2017/08/29 03:31

othersight様、回答いただきありがとうございます。 モバイルブラウザではそのような処理が行われていたんですね…初耳でした。 ダウンロード中断についても目から鱗でした。ご教授いただきありがとうございます。
Begi

2017/08/29 03:31

A-pZ様、コメントいただきありがとうございます。 Struts2での拡張というのは、自分で実装できるものなのでしょうか…?
Begi

2017/08/29 04:54

othersight様、A-pZ様 Struts2を使用せず、動画取得のHTTPリクエストを受けてレスポンスで動画ファイルを返す、ということを行えば閲覧できるのではと思ったのですがいかがでしょうか。 参考サイト:https://qa.atmarkit.co.jp/q/834
A-pZ

2017/08/29 06:45

Begi様 はい、Struts2のStreamResultでは機能が不足しているので、StreamResultを継承したクラスをつくり、resultを指定する方法で良いと思いますが、他にもサーブレットで実装してレスポンスを自前で作成する方法もできるかと思います。(StreamResult拡張については、追って調べたくなりました。ありがとうございます)
Begi

2017/08/29 10:49

A-pZ様 >StreamResultを継承したクラスを作り これはどのようにすればよろしいでしょうか…? import org.apache.struts2.convention.annotation.Result; import org.apache.struts2.convention.annotation.Results; などを追ってみましたが、Interfaceクラスで実装がどこで行われているかが分からなくて…申し訳ありません。
A-pZ

2017/08/29 12:32

Struts2のResultインタフェースを継承したクラスが、org.apache.struts2.resultパッケージにあり、ファイルのダウンロードに使っているstreamは、StreamResultクラスが動作しています。なお、新たにresultの種類を追加するので、struts.xmlの <result-types>にも新しく作るクラスを定義すれば動作します。
Begi

2017/08/30 00:24

A-pZ様 importしているstruts2のjar内のパッケージをJavaリソース内のライブラリーで確認したのですが、org.apache.struts2.resultが見当たりませんでした。以下確認したjarになります。 struts2-codebehind-plugin-2.3.24.jar struts2-convention-plugin-2.3.24.jar struts2-core-2.3.32.jar struts2-jasperreports-plugin-2.3.24.jar struts2-javatemplates-plugin-2.3.24.jar struts2-json-plugin-2.3.24.jar struts2-junit-plugin-2.3.24.jar struts2-tiles-plugin-2.3.24.jar 参照場所が間違っているのでしょうか。お手数おかけし申し訳ありません。
A-pZ

2017/08/30 00:40

なるほど、struts2.3系でしたね、失礼しました。 その場合は、org.apache.struts2.dispatcher の中にあります。
Begi

2017/08/30 02:34

A-pZ様 ありがとうございます。StreamResultクラスを見つけることが出来ました。 実装前にお伺いしたいのですが、プロジェクト内に動画を配置しvideoタグのsrcでパスを指定した場合のリクエストとレスポンスを確認したのですが、 HTTPリクエストが複数存在し、リクエストヘッダ内にはRangeがあり、0-10000、10000-20000、…のように分割してリクエストされていました。また、レスポンスヘッダにはContent-RangeでリクエストヘッダのRangeと同等の値がセットされていました。 今回はサーバ側でContent-Rangeを返すように実装すると思いますが、クライアント側のリクエストは改修する必要はありませんでしょうか。今はActionにリクエストを送っているヘッダ内にはRangeが存在していません。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだベストアンサーが選ばれていません

会員登録して回答してみよう

アカウントをお持ちの方は

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問