cancel
Showing results for 
Search instead for 
Did you mean: 

I want to read a file inside Movies which is a public folder in an app made with Unity.

eyemcastdev
Honored Guest

We are making an app to view VR videos with Unity (2020.3.21f1).
I want to play a video in the "Movies" folder, which is a public folder on my Oculus Quest device, but when I browse to a file in that folder, an "access to the path '~' is denied" error occurs.
Obviously the file exists, but it cannot be read.
Permissions are "ExternalStorageWrite", "ExternalStorageRead" both permissions are given.
The accessed folder location is "/storage/emulated/0/Movies".
"Exists" returns "True" when checking a folder using DirectoryInfo.
But even if there is a file in it, I can't read it.
How can I put a movie in the "Movies" folder and watch it in my app?

 

Android Version : Android10 API29

5 REPLIES 5

bunnyxt
Honored Guest

Hi eyemcastdev,

 

I'm now making a similar app for Oculus using Unity and faced the same problem. In my design, the user could select a local folder and then all media in the selected folder could be loaded to my app and then processed.

 

With "ExternalStorageWrite" and "ExternalStorageRead" permissions granted, the app can only access to "/storage/emulated/0/Android/data/com.example.app/files" folder. However, in most cases, the user maybe wanna select another folder.

 

Have you already solved this problem? Could you please provide some ideas about how to solve this problem?

 

Thanks!

Solved.

 

Finally, I figure out that the 'root' of accessible storage is `/sdcard` or `/storage/emulated/0`.

 

With permission 

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

(`android.permission.READ_EXTERNAL_STORAGE` is not required), I can access all the files under `/sdcard`.

 

SOSXRLeidenLab
Explorer
 
Hi! I know this thread is a little old, but I've found solutions at various sources, and I wanted to bundle the in one neat answer for future devs. My solution worked on Unity 2022, and Meta Quest 2 and 3 v65.
 
1. Get an AndroidManifest: go to --> Project Settings - Player - Publishing Settings - Build - Custom Main Manifest
This will generate a AndroidManifest.xml in the /Assets/Plugins/Android/ folder
 
2. Get permissions:
```Csharp
using UnityEngine;  
using UnityEngine.Android;  
  
  
public class GetPermissionOnAndroid : MonoBehaviour  
{  
    private void Start()  
    {  
        #if UNITY_ANDROID && !UNITY_EDITOR  
            GetExternalReadPermission();  
        #endif  
    }  
  
  
    private void GetExternalReadPermission()  
    {  
        GetPermission(Permission.ExternalStorageRead);  
    }  
  
  
    private static void GetExternalWritePermission()  
    {  
        GetPermission(Permission.ExternalStorageWrite);  
    }  
  
  
    private static void GetPermission(string permission)  
    {  
        if (Permission.HasUserAuthorizedPermission(permission))  
        {  
            Debug.Log("Permission is already granted.");  
            return;        }  
  
        Debug.LogFormat("Requesting permission to {0}.", permission);  
        Permission.RequestUserPermission(permission);  
    }  
}
```
 
3. Get the path to the Movies folder. I'm using this code, which works on Android and MacOS. Haven't been able to test this on Windows, but if anyone can comment on that, I can update if needed:
 
``` Csharp
public static class LocalPathHelpers  
{  
    public static string GetMoviesDirectoryPath()  
    {  
        var moviesPath = "";  
  
        #if UNITY_ANDROID && !UNITY_EDITOR  
        using var envClass = new UnityEngine.AndroidJavaClass("android.os.Environment");  
        using var moviesDir = envClass.CallStatic<UnityEngine.AndroidJavaObject>("getExternalStoragePublicDirectory", "Movies");       
        moviesPath = moviesDir.Call<string>("getAbsolutePath");        
        
        #elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX  
        moviesPath = System.IO.Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), "Movies");  
        
        #elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN  
        moviesPath = System.IO.Path.Combine(System.System.Environment.GetFolderPath(System.System.Environment.SpecialFolder.MyVideos));  
        
        #else  
        moviesPath = "Movies directory path is not supported on this platform.";  
        #endif  
  
        return moviesPath;  
    }  
}
```
 
Call this by using `var moviesPath = LocalPathHelpers.GetMoviesDirectoryPath();` and you should get a path to `/storage/emulated/0/Movies` on Android. 
 
4. Code for managing getting the url of the video, managing the videoplayer, and the audio
 
``` Csharp
using System;  
using System.Collections;  
using System.Collections.Generic;  
using System.Linq;  
using TMPro;  
using UnityEngine;  
using UnityEngine.Video;  
using Random = UnityEngine.Random;  
  
  
public class VideoPlayerManager2 : MonoBehaviour  
{  
    [Header("Required components")]  
    [SerializeField] private VideoPlayer m_videoPlayer;  
    [Tooltip("Put this on a child gameobject, so we can move it in space, and the sound will move with it. \n " +  
             "E.g. if you have a person speaking, and they're in a more or less fixed position, note where that is, and (in the m_clips) set the audioLocation to that position.")]  
    [SerializeField] private AudioSource m_audioSource;  
    [SerializeField] private Material m_renderMaterial;  
  
    [Header("Repeat Settings")]  
    [SerializeField] private bool m_repeatInfinitely = true;  
    [SerializeField] private bool m_playRandom = true;  
  
    [Header("Clip Settings")]  
    [SerializeField] [Range(0, 60)] private float m_beforeFirstClipPauseDuration = 5f;  
    [SerializeField] private List<VideoSettingsCustom> m_clips;  
    [SerializeField] [Range(0, 60)] private float m_afterEachClipPauseDuration = 2.5f;  
  
    [Header("Debug: Show text")]  
    [SerializeField] private TextMeshProUGUI m_infoText;  
  
  
    private RenderTexture _renderTexture;  
  
    private float _currentClipDuration;  
    private float _currentClipTime;  
    private Coroutine _playerCR;  
    private Vector2Int _dimensions;  
  
  
    private void OnValidate()  
    {  
        m_videoPlayer.source = VideoSource.Url;  
  
        for (var index = 0; index < m_clips.Count; index++)  
        {  
            var videoSetting = m_clips[index];  
  
            if (videoSetting.Set2D && videoSetting.AudioLocation != Vector3.zero)  
            {  
                Debug.LogWarning("For Clips index" + index + "the audioLocation is set to" + videoSetting.AudioLocation +  
                                 "but you also indicated that this should be a 2D sound (because for this index 'Set2D = true'). This is probably not what you want.");  
            }  
  
            if (!string.IsNullOrEmpty(videoSetting.ClipName))  
            {  
                Debug.LogWarning("ClipName cannot be empty");  
            }  
        }  
    }  
  
  
    private void Awake()  
    {  
        if (m_videoPlayer == null)  
        {  
            m_videoPlayer = GetComponentInChildren<VideoPlayer>(); // Finds it either on this object or on a child object  
        }  
  
        if (m_audioSource == null)  
        {  
            m_audioSource = GetComponentInChildren<AudioSource>(); // Put it on a child, that's best  
        }  
  
        if (m_infoText == null)  
        {  
            m_infoText = GetComponentInChildren<TextMeshProUGUI>();  
        }  
    }  
  
  
    private void Start()  
    {  
        if (m_videoPlayer == null || m_clips == null || m_clips.Count == 0 || m_audioSource == null)  
        {  
            Debug.LogError("VideoPlayer, Clips, or AudioSource not assigned.");  
  
            return;        }  
  
        _playerCR = StartCoroutine(PlayerCR());  
    }  
  
  
    private IEnumerator PlayerCR()  
    {  
        yield return new WaitForSeconds(m_beforeFirstClipPauseDuration);  
  
        do        {  
            var clipList = m_playRandom ? m_clips.OrderBy(x => Random.value).ToList() : m_clips;  
  
            foreach (var clip in clipList)  
            {  
                Debug.Log("Playing clip" + clip.ClipName + "from" + clipList.Count + "clips.");  
  
                GetURLAndPrepare(clip);  
  
                while (!m_videoPlayer.isPrepared)  
                {  
                    Debug.Log("Preparing clip");  
  
                    yield return new WaitForSeconds(0.1f);  
                }  
  
                CreateNewRenderTexture(clip);  
  
                SetAudioSourceSettings(clip);  
  
                GetCurrentClipDuration();  
  
                PlayClip();  
  
                yield return new WaitForSeconds(_currentClipDuration);  
  
                StopPlayingClip();  
  
                yield return new WaitForSeconds(m_afterEachClipPauseDuration);  
            }  
        } while (m_repeatInfinitely);  
    }  
  
  
    private void GetURLAndPrepare(VideoSettingsCustom clip)  
    {  
        var moviesPath = LocalPathHelpers.GetMoviesDirectoryPath();  
  
        m_videoPlayer.url = moviesPath + "/" + clip.ClipName;  
  
        m_videoPlayer.Prepare();  
    }  
  
  
    private void SetAudioSourceSettings(VideoSettingsCustom clip)  
    {  
        m_videoPlayer.SetTargetAudioSource(0, m_audioSource);  
  
        m_audioSource.spatialBlend = clip.Set2D ? 0 : 1;  
  
        m_audioSource.transform.position = clip.Set2D ? Vector3.zero : clip.AudioLocation;  
    }  
  
  
    private void GetCurrentClipDuration()  
    {  
        if (!m_videoPlayer.isPrepared)  
        {  
            Debug.LogWarning("VideoPlayer is not prepared yet. Cannot get the current clip duration.");  
  
            return;        }  
  
        _currentClipDuration = Mathf.Round((float) m_videoPlayer.length);
    }  
  
  
    private void CreateNewRenderTexture(VideoSettingsCustom videoSettings)  
    {  
        _dimensions.x = (int) m_videoPlayer.width;  
        _dimensions.y = (int) m_videoPlayer.height;  
        _renderTexture = new RenderTexture(_dimensions.x, _dimensions.y, 24, RenderTextureFormat.Default);  
        _renderTexture.name = "RenderTexture: " + _dimensions;  
  
        m_renderMaterial.mainTexture = _renderTexture;  
        m_videoPlayer.targetTexture = _renderTexture;  
    }  
  
  
    private void PlayClip()  
    {  
        m_videoPlayer.Play();  
    }  
  
  
    private void StopPlayingClip()  
    {  
        m_videoPlayer.Stop();  
        m_videoPlayer.clip = null;  
        m_renderMaterial.mainTexture = new RenderTexture(0, 0, 0);  
    }  
  
  
    [ContextMenu(nameof(ReshuffleVideos))]  
    public void ReshuffleVideos()  
    {  
        m_playRandom = true;  
  
        if (_playerCR != null)  
        {  
            StopPlayingClip();  
  
            StopCoroutine(_playerCR);  
            _playerCR = null;  
        }  
  
        _playerCR = StartCoroutine(PlayerCR());  
    }  
  
  
    private void Update()  
    {  
        SetInfoText();  
    }  
  
  
    private void SetInfoText()  
    {  
        if (m_infoText == null)  
        {  
            return;  
        }  
  
        if (!m_videoPlayer.isPlaying)  
        {  
            _currentClipTime = 0;  
  
            m_infoText.text = "No video playing";  
  
            return;        }  
  
        _currentClipTime = Mathf.Round((float) m_videoPlayer.clockTime);
  
        m_infoText.text = m_videoPlayer.url + "\n" + _currentClipTime + "\n" + _currentClipDuration + "\n" + _dimensions + "\n" +  
                          "Randomize: " + m_playRandom;  
    }  
  
  
    private void OnDisable()  
    {  
        StopAllCoroutines();  
    }  
}  
  
  
[Serializable]  
public class VideoSettingsCustom  
{  
    [Header("Full name, without path, but with extension. E.g. 'myVideo.mp4'")]  
    public string ClipName;  
    public bool Set2D;  
    public Vector3 AudioLocation;  
}
```
 
 
5. In the Inspector, set the name of the clips in the 'Clips' list. Don't put the full path there, but do list the extension: "Movie_1.mp4"
6. Drop the movie(s) in the Movies folder of your device (I use SideQuest for Android). Make sure the name matches perfectly with the one you set in the inspector.
 
Make sure the videos are able to be loaded / rendered. I don't know how strict Unity is. Mine were in h265 (recoded using Handbrake).
 
Good luck!

This is a perfectly timed answer! I am currently developing and running into these problems! Thank you for posting this! however;

 

   #elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN  
        moviesPath = System.IO.Path.Combine(System.System.Environment.GetFolderPath(System.System.Environment.SpecialFolder.MyVideos));  
 
This chunk of code throws an error
 
JackFollows_0-1719613591774.png

 

I haven't fully tested this solution yet but would you be able to pass me your contact details so I could contact you directly about this should I run into any problems in the future as I understand these forums are not always checked?

Thanks again for this answer and I hope to speak with you soon!

SOSXRLeidenLab
Explorer

Yes! I made an error there: there should be only 1 'System' 🙂

 

#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN
path = System.IO.Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyVideos));
#else