Android4.3以降でSDカードへ簡単にアクセスできんようになって以来MediaCodec+MediaMuxerの組み合わせでルートを取らずにSDカードの任意の場所に直接録画するのは結構面倒です。
何が?ってのは試したことが無い人のセリフで、MediaMuxerはコンストラクタにファイルパスしか受け付けんのんです(泣)
各アプリ専用の領域であれば今(Android5以降からだっけ?)は何もパーミッション無くとも読み書き可能ですし、プライマリ外部ストレージならWRITE_EXTERNAL_STORAGEパーミッションを取れば大丈夫ですが、セカンダリ以降の外部ストレージ(大抵はSDカード)の各アプリ用以外の場所は普通には書き込めません。
つまり内部メモリの一部がプライマリ外部ストレージに割り当ててあって、SDカードがセカンダリ外部ストレージ以降に割り当てられている端末…XperiaとかGALAXYとかその他諸々のSDカードを挿せる端末の殆ど…のSDカードへはアプリからは普通にはアクセスできんのです。
Android4.4(Kitkat)以降で導入されたStorage Access Framework(以下SAFね)を使えばかろうじてSDカード内へアクセス出来ます。もっともAndroid4.4でのSAFはファイル1つ毎にパーミッションを要求するという仕様で非実用的でした。
Android5以降になってディレクトリ単位でのパーミッション取得が可能となりパーミッションを取得したディレクトリ以下であれば再度パーミッションを取りなおすこと無くアクセス出来るようになりました。ファイラー系アプリはSDカードのルートディレクトリのパーミッションを取れば好き放題できるってわけです、ってSAFの存在意義ないやん。
まぁそれはともかくアクセスできるようになったんなら動画も簡単に保存できるやんと思ったあなたはがっかりすることになるのです。
SAFからは通常はUriとしてファイル位置を取得できます。SAFではこのUriとContentResolver#openOutputStreamやらContentResolver#openInputStreamを使ってファイルアクセスすることを想定しています。
でもMediaMuxerはコンストラクタにファイルパスしか受け付けんのんです?
Uriが物理ストレージ上のファイルを指していて、プライマリ(外部)ストレージ上になるのなら割と簡単にファイルパスに変換できます。セカンダリ外部ストレージ上にある場合にもゴニョゴニョすればファイルパスに変換できます。
目的のuriのUri#uri.getAuthorityの返り値がcom.android.externalstorage.documents”と一致すればそのuriは外部ストレージのどっかのファイルを指しているので、例えば次のようにすれば絶対ファイルパスを取得できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
final String docId = DocumentsContract.getDocumentId(uri); final String[] split = docId.split(":"); final String type = split[0]; if ("primary".equalsIgnoreCase(type)) { // プライマリ外部ストレージの時はEnvironment.getExternalStorageDirectoryでパスがわかる return Environment.getExternalStorageDirectory() + "/" + split[1]; } else { // プライマリストレージ以外の時は前から順に探す final String primary = Environment.getExternalStorageDirectory().getAbsolutePath(); final File[] dirs = context.getExternalFilesDirs(null); final int n = dirs != null ? dirs.length : 0; final StringBuilder sb = new StringBuilder(); for (int i = 0; i < n; i++) { final File dir = dirs[i]; if ((dir != null) && dir.getAbsolutePath().startsWith(primary)) continue; final String dir_path = dir.getAbsolutePath(); final String[] dir_elements = dir_path.split("/"); final int m = dir_elements != null ? dir_elements.length : 0; if ((m > 1) && "storage".equalsIgnoreCase(dir_elements[1])) { boolean found = false; sb.setLength(0); sb.append('/').append(dir_elements[1]); for (int j = 2; j < m; j++) { if ("Android".equalsIgnoreCase(dir_elements[j])) { found = true; break; } sb.append('/').append(dir_elements[j]); } if (found) { final File path = new File(new File(sb.toString()), split[1]); if (path.exists() && path.canWrite()) { return path.getAbsolutePath(); } } } } } |
プライマリ外部ストレージじゃない時の処理が汚いm(__)m 正規表現使うべきやろね。
ですがせっかく変換したファイルパスをMediaMuxerに渡してもだめなのです(´・ω・`)
new File(変換したファイルパス)でFileオブジェクトを生成して、File#canReadやFile#canWriteとすると読み書きできることになっています。でも実際にはUriを変換して取得したファイルパスをオープンしても読み書きできんとです。
Uriが指すファイルがプライマリ外部ストレージ上にあればWRITE_EXTERNAL_STORAGEパーミッションをとれば勿論OKですが、セカンダリ外部ストレージにはWRITE_EXTERNAL_STORAGEパーミッションがあってもアクセスできんとです(MediaMuxerがクラッシュする)。
色々試した結果、SAFでパーミッションを取得したディレクトリ以下のファイルは読み書きはできるけど通常の方法では正常にオープンできないということみたいです(´・ω・`)なんじゃそりゃ〜
で、正常にオープンするにはSAF標準の方法…つまりContentResolver#openOutputStreamやらContentResolver#openInputStreamやらを使う必要があります。
でもMediaMuxerはコンストラクタにファイルパスしか受け付けんのんです。何回言うねん–;
最終的にはContentResolver#openFileDescriptorでParcelFileDescriptorとしてファイルをオープンして、ParcelFileDescriptor#getFdでrawファイルディスクリプタとして取得、これを灰色魔術を使ってゴニョゴニョすることにしました。
肝心なところはゴニョゴニョかい(●`ε´●)えっとぉMediaMuxerは使っとりませんm(__)m
MediaMuxerの実際の処理はnative側で実装されています。MediaMuxerがどうやって書き込むファイルをnative側へ通知しているかというと、Java側のコンストラクタでファイルパスからRandomAccessFileとしてオープンしてからFileDescriptorを取得&native側へ引き渡しnative側でそこからrawファイルディスクリプタを取り出しているのです。
そのMediaMuxerはAPI18以上でしか使えませんが自分のアプリはAPI16以上なのでMediaMuxerをバックポートしています(^^)v なのでrawファイルディスクリプタを引き渡すのも簡単\(^o^)/
わっはっは^^ 結局大して役に立たん記事を書いてもうたm(__)m
20161103 アクセス数が比較的多いので追記:
実はここで書いとる内容は、密かにこっそりと黙って公開しているlibcommnというリポジトリの中にSDUtils.javaと言うヘルパークラスに実装してあるのです(^_-)-☆
さらにヘルパークラスのヘルパークラスとしてFileUtils.javaというのもありんす。
自分で使うためだけに作った(一部AOSPのソースを改変)ヘルパークラスが何と176個も^^; いやぁ働いたはたらい(笑)
build.gradleに
1 2 3 4 5 6 |
allprojects { repositories { maven { url 'http://raw.github.com/saki4510t/libcommon/master/repository/' } jcenter() } } |
と
1 2 3 4 5 6 7 |
dependencies { ... compile("com.serenegiant:common:1.1.6") { exclude module: 'support-v4' } ... } |
と書くだけで使えるようになります。jCenter?なにそれ美味しいの?(*ノω・*)テヘ
support-v4は別にexcludeせんでもいいんやけどまぁしといた方が余計なことに悩まんで済むんやろなぁ。
support-v4はあるクラスの中でローカルブロードキャストするためだけに使っとるんでまぁ自分のアプリで最新のを参照し取れば問題にはならんでしょう。
でもそれぞのヘルパークラスの使い方は勝手に想像そうてたもうれ(≧∇≦)/ 少しはコメント入っとるけどな。
不定期にクラスが無くなるかもしれんし移動するかもしれんし名前が変わるかもしれんけどな。
(古いバージョンのaarは残してあるから最新をなんて事を思わなければ使えるやろたぶん)
20180205 別の記事を書くついでに追記:
Android 8(API>=26)以降ではMediaMuxerにFileDescriptorを引数に取るコンストラクタが追加になりました。ですので、ContentResolver#openFileでParcelFileDescriptorとしてファイルをオープンしParcelFileDescriptor#getFileDescriptorの返り値を使えば簡単にMediaMuxerの初期化ができるようになりました\(^o^)/もっともまだまだAndroid8未満の端末の方が多いので簡単にはいきませんが(汗)