酢ろぐ!

カレーが嫌いなスマートフォンアプリプログラマのブログ。

Andriodで 動画ファイルを保存するとギャラリーには表示されるがサムネイルが表示されない/再生できない

FFmpeg for Andriodで書き出した動画ファイルを保存したあと、ギャラリーアプリで確認すると動画のサムネイルが表示されていない不具合があり、再生もできない。静止画を保存した場合は、特に問題なくサムネイルが表示されていた。

下記のデバイスにて検証した。Galaxシリーズの問題(?)なのかな。

  • Galaxy S6 edge / Android 7.0
  • Galaxy A7 / Android 9.0

下図左のように、ギャラリーアプリでは動画ファイルそのものは認識しているようだが、びっくりマークがついていて再生することができない。下図右のマイファイルアプリではサムネイルも再生もできる。

f:id:ch3cooh393:20200710084138p:plain

Xamarinアプリで動画を保存したらギャラリーで再生できない?

Stack Overflowで同じような現象の書き込みをみつけた。

このトピックによると、File to Fileのデータのコピー方法がおかしくても発生するようだ。

@Throws(IOException::class)
private fun copy(src: File, dst: File) {
    FileInputStream(src).use { input ->
        FileOutputStream(dst).use { out ->
            // Transfer bytes from in to out
            val buf = ByteArray(1024)
            var len: Int
            while (input.read(buf).also { len = it } > 0) {
                out.write(buf, 0, len)
            }
        }
    }
}

↓↓↓

@Throws(IOException::class)
private fun copy(src: File, dst: File) {
    src.copyTo(dst, true)
}

コピー用のメソッドを色々と変えてみたが特に現象は変わらない。コピーメソッドを修正したら治ったということは動画ファイルが壊れているのだろうか。

動画が壊れている可能性を疑う

ひょっとすると、FFmpeg for Androidで出力した動画が壊れている可能性があるのではないかと考えた。

一度PCに転送してQuickTimeで再生してみたが問題なかった。ファイルをリネームして、デバイスに転送するとギャラリーアプリに動画のサムネイルが表示された。

これは、おそらくアクセス権限の問題か、システム側で動画と認識されていないのではないかと考えた。

コンテンツプロバイダへの登録方法はさまざま

iOSでは簡単にできる「カメラロールへの保存」だが、「Android カメラロール 保存」や「Android ギャラリー 保存」でググっても、それっぽい情報は出てこない。

UIImageWriteToSavedPhotosAlbum(UIImage, nil, nil, nil)

AndroidではそもそもiOS的なカメラロールという特別な仕組みはなく、「コンテンツプロバイダ」と呼ばれる仕組みを利用している。これはどこのパスにどんな画像・動画があるのかを掌握するものだ。

コンテンツプロバイダへの登録には ContentResolverオブジェクトを使って、コンテンツデータを挿入する方法が一般的だ。ただ、文献を読むと新旧様々な手法があるようだ。

  1. 適当な場所へファイルを保存
  2. コンテンツプロバイダにファイル情報を登録

または

  1. 適当な場所へファイルを保存
  2. メディアスキャンする

または

  1. 適当な場所へファイルを保存
  2. Intent.ACTION_MEDIA_MOUNTEDをブロードキャストで投げる

などなど。

過去の歴史的経緯なのか手法が洗練されていなかったのかわからないけれど、「Android ギャラリー 保存」でググってもコレだというものがなくモヤっとした印象を受けるのはそのあたりが原因かもしれない。

コンテンツプロバイダに登録+メディアスキャンで動画が認識された

いま開発中のアプリではコンテンツプロバイダに登録後、MediaScannerConnectionを使ってファイルをスキャンさせることにした。

val resolver = context.contentResolver
val values = ContentValues().apply {
    if (isVideo) {
        put(MediaStore.Video.Media.MIME_TYPE, "video/mp4")
        put(MediaStore.Video.Media.TITLE, fileName)
        put(MediaStore.Video.Media.DATA, attachPath)
    } else {
        put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
        put(MediaStore.Images.Media.TITLE, fileName)
        put(MediaStore.Images.Media.DATA, attachPath)
    }
}

val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)

MediaScannerConnection.scanFile(context,
        arrayListOf<String>(attachPath).toTypedArray(),
        null, null)

return uri

これで最初に書いたGalaxyのギャラリーアプリに対応させることができたが、Android Q(10)以降でも同じように対応することにした。

var attachPath: String? = null

resolver.openFileDescriptor(contentUri, "w", null).use({ parcelFileDescriptor ->
    val fd = parcelFileDescriptor ?: return null
    copy(sourceFile, fd.fileDescriptor)
})
attachPath = contentUri.path

//... 中略

resolver.update(contentUri, values, null, null)

attachPath?.also {
    MediaScannerConnection.scanFile(context,
            arrayListOf<String>(it).toTypedArray(),
            null, null)
}

return contentUri

コンテンツプロバイダに登録している情報が少ないのが本件の原因だと思うがギャラリーアプリ側でももっと頑張って欲しい。対応としてはメディアスキャンだけでも良さそうな気もするが、デバイスによっては MediaScannerConnection のスキャンだけだと失敗する事例もあるようだ。

最終的に コンテンツプロバイダへの登録 -> メディアスキャン という方法で決着をつけた。

参照記事