1

I've been trying to take a screenshot and then immediately after, use it to show some sort of preview and some times it works and some times it doesn't, I'm currently not at work and I don't have unity in this computer so I'll try to recreate it on the fly (there might be some syntax mistakes here and there)

public GameObject screenshotPreview;

public void TakeScreenshot () {

        string imageName = "screenshot.png";

        // Take the screenshot
        ScreenCapture.CaptureScreenshot (imageName);

        // Read the data from the file
        byte[] data = File.ReadAllBytes(Application.persistentDataPath + "/" + imageName);

        // Create the texture
        Texture2D screenshotTexture = new Texture2D(Screen.width, Screen.height);

        // Load the image
        screenshotTexture.LoadImage(data);

        // Create a sprite
        Sprite screenshotSprite = Sprite.Create (screenshotTexture, new Rect(0, 0, Screen.width, Screen.height), new Vector2(0.5f, 0.5f) );

        // Set the sprite to the screenshotPreview
        screenshotPreview.GetComponent<Image> ().sprite = screenshotSprite;

}

As far as I've read, ScreenCapture.CaptureScreenshot is not async so the image should have been written right before I try to load the data, but the problem is as I've said before some times it doesn't work and it loads an 8x8 texture with a red question mark, which apparently is the texture failing to be loaded but the file should've been there so I cannot understand why it's not getting loaded properly.

another thing I've tried (which is disgusting but I'm getting tired of this and running out of ideas) is to put in the update method to wait for some time and then execute the code to load the data and create the texture, sprite and display it but even so, it fails some times, less frequently than before but it still fails, which leads me to belive that even if the file was created it hasn't finish beign written, does anyone know a workaround to this? any advice is appreciated.

As extra information this project is being run in an iOS device.

Programmer
  • 121,791
  • 22
  • 236
  • 328
Baek Ryun
  • 100
  • 1
  • 3
  • 14

3 Answers3

4

The ScreenCapture.CaptureScreenshot function is known to have many problems. Here is another one of it.

Here is a quote from its doc:

On Android this function returns immediately. The resulting screenshot is available later.

The iOS behavior is not documented but we can just assume that the behavior is the-same on iOS. Wait for few frames after taking the screenshot before you attempt to read/load it.

public IEnumerator TakeScreenshot()
{

    string imageName = "screenshot.png";

    // Take the screenshot
    ScreenCapture.CaptureScreenshot(imageName);

    //Wait for 4 frames
    for (int i = 0; i < 5; i++)
    {
        yield return null;
    }

    // Read the data from the file
    byte[] data = File.ReadAllBytes(Application.persistentDataPath + "/" + imageName);

    // Create the texture
    Texture2D screenshotTexture = new Texture2D(Screen.width, Screen.height);

    // Load the image
    screenshotTexture.LoadImage(data);

    // Create a sprite
    Sprite screenshotSprite = Sprite.Create(screenshotTexture, new Rect(0, 0, Screen.width, Screen.height), new Vector2(0.5f, 0.5f));

    // Set the sprite to the screenshotPreview
    screenshotPreview.GetComponent<Image>().sprite = screenshotSprite;

}

Note that you must use StartCoroutine(TakeScreenshot()); to call this function.


If that did not work, don't use this function at-all. Here is another way to take and save screenshot in Unity:

IEnumerator captureScreenshot()
{
    yield return new WaitForEndOfFrame();

    string path = Application.persistentDataPath + "Screenshots/"
            + "_" + screenshotCount + "_" + Screen.width + "X" + Screen.height + "" + ".png";

    Texture2D screenImage = new Texture2D(Screen.width, Screen.height);
    //Get Image from screen
    screenImage.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);
    screenImage.Apply();
    //Convert to png
    byte[] imageBytes = screenImage.EncodeToPNG();

    //Save image to file
    System.IO.File.WriteAllBytes(path, imageBytes);
}
Programmer
  • 121,791
  • 22
  • 236
  • 328
  • I was just reading [here](https://developer.vuforia.com/forum/faq/unity-how-can-i-capture-screen-shot) about that way but the fact that you proposed it makes me feel better about it and I'll give it a shot tomorrow, Thanks! – Baek Ryun Sep 29 '17 at 06:23
  • 1
    I think the second example without `ScreenCapture` is not async and will block the UI thread especially in `EncodeToPNG` (you might want `EncodeToJPG` for way smaller file sizes - factor might be over 10.) ... nevertheless it seems to be more relyable ^^ you could also improve the UI.block a bit further by writing the result [async](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/using-async-for-file-access) – derHugo Jan 10 '19 at 08:26
  • In the first method, what is the significance of 5 frames? – Mars Jan 10 '19 at 09:02
  • @Mars it's 4 frames (5 is exclusive). However I agree: Since in the docu it says `The CaptureScreenshot returns immediately on Android. The screen capture continues in the background. The resulting screen shot is saved in the file system after a few seconds.` one should maybe rather use `yield return new WaitForSeconds(XY)` to actually wait a few **seconds** not frames – derHugo Jan 10 '19 at 11:02
  • @derHugo 0-1-2-3-4 is 5 frames, but yeah, I don't think waiting xy seconds is a guarantee either. Unless maybe there is a known timeout for CaptureScreenshot – Mars Jan 11 '19 at 00:22
  • @Mars ups you're totally right ofcourse :'D and yeah usually there should be a callback for async methods (like e.g. with PhotoCapture) no clue why they didn't implement one for ScreenCapture as well. That's why I'ld say the second solution provided here might block the UI thread but at least it is reliable – derHugo Jan 11 '19 at 06:29
  • @derHugo Very true. However, at the time of writing this answer, async-await was an "experimental" feature. Haven't used Unity in a few years though, so don't know how things are now! That said, theres no reason you couldn't create a Thread to do it, or (if supported by mono) a Task – Mars Jan 11 '19 at 08:18
  • Thanks for the second variant. It's hard to belive that ScreenCapture.CaptureScreenshot is so crappy in a game engine of this caliber... I had problems rendering TextMeshPro billboards using it. Instant success with the other approach. – Avrdan Dec 02 '21 at 19:59
1

Programmer's code worked successfully by being called like the following. It is designed as coroutine, so it would not interfere frame rate. Hence it should be called as coroutine. Make sure the CallerObject is inheriting from "MonoBehaviour".

 public class CallerObject : MonoBehaviour
{
    public void Caller()
    {
        String imagePath = Application.persistentDataPath + "/image.png";
        StartCoroutine(captureScreenshot(imagePath));
    }


    IEnumerator captureScreenshot(String imagePath)
    {
        yield return new WaitForEndOfFrame();
        //about to save an image capture
        Texture2D screenImage = new Texture2D(Screen.width, Screen.height);


        //Get Image from screen
        screenImage.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);
        screenImage.Apply();

        Debug.Log(" screenImage.width" + screenImage.width + " texelSize" + screenImage.texelSize);
        //Convert to png
        byte[] imageBytes = screenImage.EncodeToPNG();

        Debug.Log("imagesBytes=" + imageBytes.Length);

        //Save image to file
        System.IO.File.WriteAllBytes(imagePath, imageBytes);
    }
}
Lenvanthis
  • 71
  • 1
  • 6
  • Coroutines are still run on the mainthread and will interfere with your framerate – Mars Jan 15 '19 at 01:30
  • The point is that the process will not wait until your image capture is done. It will share the process time if it is working as designed, even if it is running on the main thread. – Lenvanthis Jan 16 '19 at 02:53
  • What here makes it not wait? If I understand this code correctly, It waits until the end of the frame instead of during update, but if it takes 2 seconds to read/write, then that one frame will still be over 2 seconds long – Mars Jan 16 '19 at 04:03
  • The process will not wait for image capture is done. But the image capture will wait for the end of frame. – Lenvanthis Jan 17 '19 at 05:55
  • So whichever phase you call this function in will finish, but your frame will still take 2 seconds. This won't save your framerate – Mars Jan 17 '19 at 05:59
  • If you want to confirm, try adding `Thread.Sleep(10000);` after your yield! (Sorry, I can't check as I don't have Unity at work) – Mars Jan 17 '19 at 06:03
0

I see nothing in the docs that says its not Async. In fact, for Android (if I'm reading this correctly), it explicitly says it's async.

That said, I'd try stalling while the file is not found. Throw it in a coroutine and

FileInfo yourFile = new FileInfo("YourFile.png");
while (File.Exists(yourFile.name) || IsFileLocked(yourFile)) 
    yield return null;

IsFileLocked

You could also try throwing in some debug checks in there to see how long it takes (seconds or frames) before the file appears (assuming it ever appears).

Edit: As derHugo pointed out, the file existing doesn't mean the file is ready yet. I have edited the code to handle that! But it still doesn't cover the case where the file already existed, in which case you probably want a dynamic file name like with a timestamp, or you want to delete the file first!

Mars
  • 2,505
  • 17
  • 26
  • I might be wrong but couldn't the file be already found by `file.found` while the stream is still writing to it? And ofcourse this might also not work if you want to override the file instead of creating a new one everytime – derHugo Jan 10 '19 at 08:29
  • @derHugo Sorry, forgot to tag you – Mars Jan 10 '19 at 09:05