jiangdongguo / AndroidUSBCamera

🔥🔥🔥Flexible and useful UVC camera engine on Android platform, supporting multi-road cameras!
https://juejin.cn/post/7115229806844706847
Apache License 2.0
2.24k stars 760 forks source link

Captured pictures and videos has 0 byte size, but only until next reboot #618

Open OlavAa opened 8 months ago

OlavAa commented 8 months ago

First of all: When I build the demo app, everything works. But when I use the libraries of the demo app and make my own fragment then I observe problems:

Capture of photo and video is effortless. I even get the correct preview. But when I enter Google Photos the miniatures have no preview image. And when I open a photo or a video captured, the meta data is present (like file name, lenght of the video and so on). But the ize is 0 byte. Pictures cannot be viewed when opened, videos played end in an error. But - if I reboot the Android device all files are OK. So it is a temporary problem.

My theory is that the problem lies within the RenderManager.kt method: saveInternalImage.

This method first saves the file using FileOutputStream. And if successful, then inserts the meta data into the MediaStore. Which seems logical. But this is not the predicted way in Android Developer. Not nearly as intuitive they suggest to insert in mediastore first, and then write the content using URI rather than path. In addition, the "values.put(MediaStore.Images.ImageColumns.DATA, path)" is now deprecated. I spent quite some time before I finally got this to work. For now only for saveImageInternal. The video capture is still a little buggy for me and also requires another class to be changed.

Not saying the methods in the demo application are wrong. Because the demo app indeed works very well. But it also works well with the suggested changed I include below. And with these changes, so does the fragment I wrote myself (largely based on the structure of the DemoFragment). The suggestion should also handle Android versions lesser than or greater/equal than Android 10 as well. In theory at least. I have no sub Android 10 devices to use for testing and only tested this with Android 10, 11 and 13.

Main changes in this updated method Android 10+:

  1. MediaStore insert before saving using output stream and compress
  2. MIME_TYPE included as value
  3. Android 10+: No path used, instead relative path
  4. Insert based on URI

And finally for Android < 10, and mot yet tested: Alternative build method without MediaStore and use of Media Scanner to refresh at the end.

Alternative method suggested below:

`private fun saveImageInternal(savePath: String?) { if (mCaptureState.get()) { return } mCaptureState.set(true) mMainHandler.post { mCaptureDataCb?.onBegin() }

    val date = System.currentTimeMillis()
    val title = savePath ?: "IMG_AUSBC_${mDateFormat.format(date)}"
    val displayName = savePath ?: "$title.jpg"
    val width = mWidth
    val height = mHeight

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        // Android 10 and above
        val values = ContentValues().apply {
            put(MediaStore.Images.Media.TITLE, title)
            put(MediaStore.Images.Media.DISPLAY_NAME, displayName)
            put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
            put(MediaStore.Images.Media.DATE_TAKEN, date)
            put(MediaStore.Images.Media.WIDTH, width)
            put(MediaStore.Images.Media.HEIGHT, height)
            put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
        }

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

        uri?.let {
            try {
                resolver.openOutputStream(uri).use { outputStream ->
                    GLBitmapUtils.transFrameBufferToBitmap(mFBOBufferId, width, height).apply {
                        compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
                        recycle()
                    }
                }
                mMainHandler.post {
                    mCaptureDataCb?.onComplete(uri.toString())
                }
            } catch (e: IOException) {
                Logger.e(TAG, "Failed to write file, err = ${e.localizedMessage}", e)
                mMainHandler.post {
                    mCaptureDataCb?.onError(e.localizedMessage)
                }
            }
        } ?: run {
            Logger.e(TAG, "Failed to insert file into MediaStore")
            mMainHandler.post {
                mCaptureDataCb?.onError("Failed to insert file into MediaStore")
            }
        }
    } else {
        // Below Android 10
        val path = savePath ?: "$mCameraDir/$displayName"
        var fos: FileOutputStream? = null
        try {
            fos = FileOutputStream(path)
            GLBitmapUtils.transFrameBufferToBitmap(mFBOBufferId, width, height).apply {
                compress(Bitmap.CompressFormat.JPEG, 100, fos)
                recycle()
            }
        } catch (e: IOException) {
            Logger.e(TAG, "Failed to write file, err = ${e.localizedMessage}", e)
            mMainHandler.post {
                mCaptureDataCb?.onError(e.localizedMessage)
            }
        } finally {
            try {
                fos?.close()
            } catch (e: IOException) {
                Logger.e(TAG, "Failed to close FileOutputStream, err = ${e.localizedMessage}", e)
            }
        }

        // Notify the MediaScanner
        MediaScannerConnection.scanFile(mContext, arrayOf(path), null, null)

        mMainHandler.post {
            mCaptureDataCb?.onComplete(path)
        }
    }`

Tested on Android 10+ this eliminated my issues with pictures that were not possible to view after capture, only after device reboot

OlavAa commented 8 months ago

I also had severe issues building the demo app using newer NDK versions. Probably more NDK versions that works.

But at least this works: android-ndk-r21e

ronaldsampaio commented 5 months ago

I applied your solution in the 3.3.3 version fork that I made. It works thanks!