11

To record a SurfeceView I'm using a 3rd-party library , this library requires a path where the output (the recorded video) saved in my case is savedVideoPath :

mRenderPipeline = EZFilter.input(this.effectBmp)
                .addFilter(new Effects().getEffect(VideoMaker.this, i))
                .enableRecord(savedVideoPath, true, false)
                .into(mRenderView);

After the recording stopped, the video should be saved with savedVideoPath as a path, when I test the code, that is to say , when I open the gallery app, I see the saved video there, but when I tested on Android Q, I can't see anything.

Since getExternalStoragePublicDirectory and getExternalStorageDirectory are deprecated ,I tried to use getExternalFilesDir as following :

private void getPath() {
    String videoFileName = "video_" + System.currentTimeMillis() + ".mp4";
    fileName = videoFileName;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        File imageFile = null;
        File storageDir = new File(
            getExternalFilesDir(Environment.DIRECTORY_MOVIES), 
            "Folder");
        source = storageDir;
        boolean success = true;
        if (!storageDir.exists()) {
            success = storageDir.mkdirs();
        }
        if (success) {
            imageFile = new File(storageDir, videoFileName);
            savedVideoPath = imageFile.getAbsolutePath();
        }
    } else {
        File storageDir = new File(
            Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES)
            + "/Folder");
        boolean success = true;
        if (!storageDir.exists()) {
            success = storageDir.mkdirs();
        }
        if (success) {
            File videoFile = new File(storageDir, videoFileName);
            savedVideoPath = videoFile.getAbsolutePath();
        }
    }
}

After the recording stopped, I go to Files Explorer app > Android > data > com.packageName > files > Movies > Folder ,I can see all saved videos there,but I can't see them on the gallery.

I tried to use Intent.ACTION_MEDIA_SCANNER_SCAN_FILE to refresh the gallery, but unfortunately doesn't work.

I also tried MediaScannerConnection:

MediaScannerConnection.scanFile(
    context, 
    new String[]{savedVideoPath}, 
    new String[]{"video/mp4"}, 
    new MediaScannerConnection.MediaScannerConnectionClient() {

    public void onMediaScannerConnected() {
    }

    public void onScanCompleted(String s, Uri uri) {
    }
});
  • Can anyone help me to get resolve this issue? I stuck on it for almost 2 days
Genusatplay
  • 761
  • 1
  • 4
  • 15
  • 1
    Try `MediaScannerConnection` and its `scanFile()` method. Note that it is possible that you will not be able to satisfy both conditions (have filesystem access and have the video appear in `MediaStore` for gallery apps) with a single file. – CommonsWare Sep 13 '19 at 21:17
  • @CommonsWare ,thank you for your comment, I have tried it , but unfortunately, no video found in Gallery – Mouaad Abdelghafour AITALI Sep 13 '19 at 21:27
  • That library probably could be adapted to use [the `MediaMuxer` constructor that takes a `FileDescriptor`](https://developer.android.com/reference/android/media/MediaMuxer.html#MediaMuxer(java.io.FileDescriptor,%20int)). Then, you would be able to use `openFileDescriptor()` on `ContentResolver` to be able to go back to your `MediaStore` `Uri` issue. Otherwise, after you have modified the video with the library, copy it into the `MediaStore` (see your previous question) and delete your file copy. – CommonsWare Sep 13 '19 at 22:08
  • @CommonsWare thank you for your comment, copy the path or the file itself? , and how I can copy it to `MediaStore` ,thank you – Mouaad Abdelghafour AITALI Sep 13 '19 at 22:20
  • "copy the path or the file itself?" -- the file. "how I can copy it to MediaStore" -- we discussed this in [an earlier question](https://stackoverflow.com/q/57923329/115145). The code that you have there, with the modifications in my answer, should work fine... just use your converted video file as the source of data. [This class](https://gitlab.com/commonsguy/cw-android-q/blob/v0.5/ConferenceVideos/src/main/java/com/commonsware/android/conferencevideos/VideoRepository.kt) shows downloading a video; your code will be the same, just using a file as your data source. – CommonsWare Sep 13 '19 at 23:10
  • @CommonsWare Can you take look into https://stackoverflow.com/questions/63222644/contentresolver-query-doesnt-return-the-newly-inserted-video? – Gunaseelan Aug 03 '20 at 01:48
  • search for `.NOMEDIA` file –  Sep 25 '21 at 01:36

4 Answers4

17

You have to change the library to make it work with Android Q. If you cannot do this you could copy the video to the media gallery and then delete the old video created in getExternalFilesDir(). After this you have the URI of the video and can do what you want with the URI.

If you have saved the video with getExternalFilesDir() you could use my example here: The media URI you get is "uriSavedVideo". This is only an example. A large video should also be copied in the background.

Uri uriSavedVideo;
File createdvideo = null;
ContentResolver resolver = getContentResolver();
String videoFileName = "video_" + System.currentTimeMillis() + ".mp4";
ContentValues valuesvideos;
valuesvideos = new ContentValues();

if (Build.VERSION.SDK_INT >= 29) {
    valuesvideos.put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/" + "Folder");
    valuesvideos.put(MediaStore.Video.Media.TITLE, videoFileName);
    valuesvideos.put(MediaStore.Video.Media.DISPLAY_NAME, videoFileName);
    valuesvideos.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
    valuesvideos.put(
        MediaStore.Video.Media.DATE_ADDED, 
        System.currentTimeMillis() / 1000);

    Uri collection = 
        MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
    uriSavedVideo = resolver.insert(collection, valuesvideos);
} else {
    String directory  = Environment.getExternalStorageDirectory().getAbsolutePath() 
    + File.separator + Environment.DIRECTORY_MOVIES + "/" + "YourFolder";
    createdvideo = new File(directory, videoFileName);

    valuesvideos.put(MediaStore.Video.Media.TITLE, videoFileName);
    valuesvideos.put(MediaStore.Video.Media.DISPLAY_NAME, videoFileName);
    valuesvideos.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
    valuesvideos.put(
        MediaStore.Video.Media.DATE_ADDED, 
        System.currentTimeMillis() / 1000);
    valuesvideos.put(MediaStore.Video.Media.DATA, createdvideo.getAbsolutePath());

    uriSavedVideo = getContentResolver().insert(
        MediaStore.Video.Media.EXTERNAL_CONTENT_URI, 
        valuesvideos);
}

if (Build.VERSION.SDK_INT >= 29) {
    valuesvideos.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis());
    valuesvideos.put(MediaStore.Video.Media.IS_PENDING, 1);
}

ParcelFileDescriptor pfd;
try {
    pfd = getContentResolver().openFileDescriptor(uriSavedVideo, "w");

    FileOutputStream out = new FileOutputStream(pfd.getFileDescriptor());
    // get the already saved video as fileinputstream
    // The Directory where your file is saved
    File storageDir = new File(
        getExternalFilesDir(Environment.DIRECTORY_MOVIES), 
        "Folder");
    //Directory and the name of your video file to copy
    File videoFile = new File(storageDir, "Myvideo"); 
    FileInputStream in = new FileInputStream(videoFile);

    byte[] buf = new byte[8192];
    int len;
    while ((len = in.read(buf)) > 0) {
        out.write(buf, 0, len);
    }

    out.close();
    in.close();
    pfd.close();
} catch (Exception e) {
    e.printStackTrace();
}

if (Build.VERSION.SDK_INT >= 29) {
    valuesvideos.clear();
    valuesvideos.put(MediaStore.Video.Media.IS_PENDING, 0);
    getContentResolver().update(uriSavedVideo, valuesvideos, null, null);
}
Laurel
  • 5,965
  • 14
  • 31
  • 57
Frank
  • 2,738
  • 2
  • 14
  • 19
  • 1
    The best way for Android Q and above is this. Thanks a lot! – mahdi azarm Aug 12 '20 at 06:04
  • Some users for me are reporting issues. Crash report says `java.lang.UnsupportedOperationException` on `resolver.insert()` - Any ideas? Is it possible they don't have external storage? – JCutting8 Nov 04 '20 at 12:07
  • Hi consider that MediaStore.Video.Media.DATE_TAKEN and MediaStore.Video.Media.IS_PENDING is only for APi >= 29 !!! But you can continue also the old method with file because Android 11 supports the file methods again for "media files" (look at the docs for scoped storage) . To make this work on Android 10 too just add android:requestLegacyExternalStorage="true" to your application in manifest. – Frank Nov 04 '20 at 15:55
  • Nope - on older devices getting `java.lang.UnsupportedOperationException: Unknown URI: content://media/external_primary/video/media` so it doesn't seem to be related to `DATE_TAKEN` or `IS_PENDING` – JCutting8 Nov 05 '20 at 12:36
  • Hi JCutting8. I have updated the complete example to show you the difference between Api >=29 and lower 29 . So i think now you can understand the differences. There are some differences how to insert the video and get the uri. This should work without any problems. Only adjust things like filenames and directories – Frank Nov 05 '20 at 14:18
  • Can you explain what all should we change? – WebDiva Dec 09 '20 at 15:41
  • Hello WebDiva ! Sorry but you must also understand what you are doing ! Learning by doing and not let the others do it. I have learned everything by myself. Just read many documentations and write a test app first . This is a complete example and you must be able to understand what everything means. – Frank Dec 09 '20 at 20:18
  • java.lang.UnsupportedOperationException: Unknown URI: content://media/external_primary/video/media is coming because android Q is not allowing passing any path that starts from external storage in RELATIVE_PATH column – Ravikant Tiwari Mar 05 '21 at 16:13
12

Here it is my solution - save photo/video to Gallery.

private fun saveMediaFile2(filePath: String?, isVideo: Boolean, fileName: String) {
filePath?.let {
    val context = MyApp.applicationContext
    val values = ContentValues().apply {
        val folderName = if (isVideo) {
            Environment.DIRECTORY_MOVIES
        } else {
            Environment.DIRECTORY_PICTURES
        }
        put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
        put(MediaStore.Images.Media.MIME_TYPE, MimeUtils.guessMimeTypeFromExtension(getExtension(fileName)))
        put(MediaStore.Images.Media.RELATIVE_PATH, folderName + "/${context.getString(R.string.app_name)}/")
        put(MediaStore.Images.Media.IS_PENDING, 1)
    }

    val collection = if (isVideo) {
        MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
    } else {
        MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
    }
    val fileUri = context.contentResolver.insert(collection, values)

    fileUri?.let {
        if (isVideo) {
            context.contentResolver.openFileDescriptor(fileUri, "w").use { descriptor ->
                descriptor?.let {
                    FileOutputStream(descriptor.fileDescriptor).use { out ->
                        val videoFile = File(filePath)
                        FileInputStream(videoFile).use { inputStream ->
                            val buf = ByteArray(8192)
                            while (true) {
                                val sz = inputStream.read(buf)
                                if (sz <= 0) break
                                out.write(buf, 0, sz)
                            }
                        }
                    }
                }
            }
        } else {
            context.contentResolver.openOutputStream(fileUri).use { out ->
                val bmOptions = BitmapFactory.Options()
                val bmp = BitmapFactory.decodeFile(filePath, bmOptions)
                bmp.compress(Bitmap.CompressFormat.JPEG, 90, out)
                bmp.recycle()
            }
        }
        values.clear()
        values.put(if (isVideo) MediaStore.Video.Media.IS_PENDING else MediaStore.Images.Media.IS_PENDING, 0)
        context.contentResolver.update(fileUri, values, null, null)
    }
}
}
nAkhmedov
  • 3,522
  • 4
  • 37
  • 72
  • Hi, this solution is awesome! but for video, I keep getting `FileNotFoundException` as the video is under the same app-directory as image. Image just working perfectly. Any idea on this? – Teo Apr 16 '21 at 01:59
2

Thanks to other solutions I managed to get done with this code:

fun saveVideo(filePath: String?, isMOV: Boolean, fileName: String) {
     filePath?.let {
        val context = requireContext()
        val values = ContentValues().apply {
            val folderName = Environment.DIRECTORY_MOVIES

            put(MediaStore.Video.Media.DISPLAY_NAME, fileName)
            put(MediaStore.Video.Media.TITLE, fileName)
            put(
                MediaStore.Video.Media.MIME_TYPE, if (isMOV) {
                    "video/quicktime"
                } else {
                    "video/mp4"
                }
            )
            if (Build.VERSION.SDK_INT >= 29) {
                put(
                    MediaStore.Video.Media.RELATIVE_PATH,
                    folderName + "/${context.getString(R.string.app_name)}"
                )
                put(
                    MediaStore.Video.Media.DATE_ADDED,
                    System.currentTimeMillis() / 1000
                )
                put(MediaStore.Video.Media.IS_PENDING, 1)

            } else {
                put(
                    MediaStore.Video.Media.DATE_ADDED,
                    System.currentTimeMillis() / 1000
                )

            }
        }
        val fileUri = if (Build.VERSION.SDK_INT >= 29) {
            val collection =
                MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
            context.contentResolver.insert(collection, values)
        } else {
            requireContext().contentResolver.insert(
                MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
                values
            )!!
        }
        fileUri?.let {
            context.contentResolver.openFileDescriptor(fileUri, "w").use { descriptor ->
                descriptor?.let {
                    try {
                        FileOutputStream(descriptor.fileDescriptor).use { out ->
                            val videoFile = File(filePath)
                            FileInputStream(videoFile).use { inputStream ->
                                val buf = ByteArray(8192)
                                while (true) {
                                    val sz = inputStream.read(buf)
                                    if (sz <= 0) break
                                    out.write(buf, 0, sz)
                                }
                            }
                        }
                    } catch (e: Exception) {
                        Toast.makeText(context, "couldn't save the video", Toast.LENGTH_SHORT)
                            .show()
                        return
                    }
                }
            }

            values.clear()
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                values.put(MediaStore.Video.Media.IS_PENDING, 0)
            }
            try {
                requireContext().contentResolver.update(fileUri, values, null, null)
            }catch (e:Exception){}
        }
    }
    Toast.makeText(context, "Video saved to gallery", Toast.LENGTH_SHORT).show()
}
Khaled Mahmoud
  • 285
  • 2
  • 7
1

The reply is late but may benefit someone in the future. here is my version of code perfectly working in android 26 to 31. Lower than 26 I did not check. Need to implement Apache commons Io.

suspend fun moveVideo1(context: Context,src: String,videoFileName: String,extension: String): Boolean
{
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
    {
        val valueVideo: ContentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.RELATIVE_PATH,getRelativeDownloadFolder())
            put(MediaStore.MediaColumns.TITLE,videoFileName)
            put(MediaStore.MediaColumns.DISPLAY_NAME,videoFileName)
            put(MediaStore.MediaColumns.MIME_TYPE, MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension))
            put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000)
            put(MediaStore.MediaColumns.DATE_TAKEN, System.currentTimeMillis());
            put(MediaStore.MediaColumns.IS_PENDING, 1);
        }

        val uriSavedVideo = context.contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI,valueVideo)?: Uri.EMPTY

        val pfd: ParcelFileDescriptor?

        try {
            pfd = context.contentResolver.openFileDescriptor(uriSavedVideo, "w")
            val out = FileOutputStream(pfd?.fileDescriptor)
            val inStream = FileInputStream(File(src))
            val buf = ByteArray(8192)

            var len: Int
            while (inStream.read(buf).also { len = it } > 0) {
                out.write(buf, 0, len)
            }
            out.close()
            inStream.close()
            pfd?.close()
            File(src).delete()
        }catch (e: Exception) {
            e.printStackTrace()
            return false
        }

        valueVideo.clear();
        valueVideo.put(MediaStore.MediaColumns.IS_PENDING, 0);
        context.contentResolver.update(uriSavedVideo, valueVideo, null, null);
        return true
    }
    else
    {
        try {
            FileUtils.moveFile(File(src),File(getDefaultDownloadFolder(context), "$videoFileName.$extension"))
        }catch (e: Exception) {
            e.printStackTrace()
            return false
        }
        return true
    }
}