We are developing with Unity 2018.4.22f1.
Obtain the thumbnail image of the frame specified as 10 frame point, 20 frame point… from the video file to be played by VideoPlayer.
It is supposed to be displayed as a list.
Does VideoPlayer have a function to get thumbnail images?
Or is there an alternative?
Thank you.
Hi!
There is no direct way of doing what you are asking, but there is one approach that exists. However, depending on platform, this approach seems to have gotchas (see Skipping to video frame not working on Android ).
So:
- Create a VideoPlayer and turn off its playOnAwake property so it won’t start automatically playing your clip;
- Set the clip for which you want to read frames;
- Set the render mode to API only so it won’t try to draw in the scene;
- Enable the frameReady events;
- Add a frameReady handler to your VideoPlayer which will be where you capture the frame;
- Seek to the first frame you want to capture by setting the VideoPlayer’s current frame;
- Call VideoPlayer.Pause(), which will bootstrap the decoding and acquire graphics resources for rendering, but without starting playback;
- When the first frame is ready, your handler will be invoked;
- Capture the VideoPlayer’s texture into a Texture2D using the method outlined here: Unity - Scripting API: RenderTexture.active
- Change the VideoPlayer’s current frame to the next frame you want to capture;
- Your frameReady handler will be invoked again (like it did at step 8);
- And so on.
As mentioned above, there are bugs in there that may limit your success in doing this, but this is the closest we currently have to a solution.
Let us know how this goes,
Dominique Leroux
A/V developer at Unity
4 Likes
I had trouble with directly using the above information, but managed to find success with some edits and from Programmer’s currently accepted reply to this post. I’ve expanded that into a tool for myself and am dropping it here.
Known downsides:
- Asynchronous, you aren’t necessarily going to get a same-frame return, that’s just prepping the video player though
- If mishandled or used often, you could run into memory issues far enough down the line. This works for us as a museum app that processes videos in a resources folder at runtime (so they can be swapped out easily).
Best of luck!
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Video;
public static class ThumbnailCreator
{
private struct ThumbnailRequest
{
public VideoClip ClipToProcess;
public string UrlToProcess;
public int FrameToCapture;
public ThumnailCreated callback;
}
public delegate void ThumnailCreated(Sprite thumbnail);
private static VideoPlayer videoPlayer
{
get
{
if(_videoPlayer == null)
{
GameObject ThumbnailProcessor = new GameObject("Thumbnail Processor");
Object.DontDestroyOnLoad(ThumbnailProcessor);
_videoPlayer = ThumbnailProcessor.AddComponent<VideoPlayer>();
_videoPlayer.renderMode = VideoRenderMode.APIOnly;
_videoPlayer.playOnAwake = false;
_videoPlayer.audioOutputMode = VideoAudioOutputMode.None;
}
return _videoPlayer;
}
}
private static VideoPlayer _videoPlayer;
private static ThumnailCreated thumbNailCreatedCallback;
private static bool processInProgress;
private static Queue<ThumbnailRequest> thumbnailQueue = new Queue<ThumbnailRequest>();
/// <summary>
/// Internal Function used only to consume enqueued requests
/// </summary>
private static void GetThumbnailFromVideo(ThumbnailRequest request)
{
if(request.ClipToProcess != null)
{
GetThumbnailFromVideo(request.ClipToProcess, request.callback, request.FrameToCapture);
}
else if (request.UrlToProcess != null)
{
GetThumbnailFromVideo(request.UrlToProcess, request.callback, request.FrameToCapture);
}
}
/// <summary>
/// Creates a thumbnail from the string video passed in. The thumbnail will be asynchronously passed to the callback function
/// </summary>
/// <param name="videoURL">The URL to load the video from</param>
/// <param name="callback">Where to pass the created Sprite</param>
public static void GetThumbnailFromVideo(string videoURL, ThumnailCreated callback, int frameToCapture = 0)
{
if(string.IsNullOrEmpty(videoURL))
{
Debug.LogWarning("Null videoURL detected. Unable to generate thumbnail.");
return;
}
if(processInProgress)
{
thumbnailQueue.Enqueue(new ThumbnailRequest()
{
UrlToProcess = videoURL,
FrameToCapture = frameToCapture,
callback = callback
});
return;
}
processInProgress = true;
thumbNailCreatedCallback = callback;
videoPlayer.source = VideoSource.Url;
videoPlayer.url = videoURL;
PrepareVideoForProcessing(frameToCapture);
}
/// <summary>
/// Creates a thumbnail from the string video passed in. The thumbnail will be asynchronously passed to the callback function
/// </summary>
/// <param name="videoClip">The clip to take the screenshot from</param>
/// <param name="callback">Where to pass the created Sprite</param>
public static void GetThumbnailFromVideo(VideoClip videoClip, ThumnailCreated callback, int frameToCapture = 0)
{
if(videoClip == null)
{
Debug.LogWarning("Null videoURL detected. Unable to generate thumbnail.");
return;
}
if (processInProgress)
{
thumbnailQueue.Enqueue(new ThumbnailRequest()
{
ClipToProcess = videoClip,
FrameToCapture = frameToCapture,
callback = callback
});
return;
}
processInProgress = true;
thumbNailCreatedCallback = callback;
videoPlayer.source = VideoSource.VideoClip;
videoPlayer.clip = videoClip;
PrepareVideoForProcessing(frameToCapture);
}
private static void PrepareVideoForProcessing(int frameToCapture)
{
videoPlayer.sendFrameReadyEvents = true;
videoPlayer.frameReady += ThumbnailReady;
videoPlayer.frame = frameToCapture;
// TODO This is bugged in Unity, check in the future if we can remove play and still recieve the frame with just .frame and Pause()
videoPlayer.Play();
videoPlayer.Pause();
}
private static void ThumbnailReady(VideoPlayer source, long frameIdx)
{
videoPlayer.sendFrameReadyEvents = false;
videoPlayer.frameReady -= ThumbnailReady;
Texture2D tex = new Texture2D(2, 2);
RenderTexture renderTexture = source.texture as RenderTexture;
if (tex.width != renderTexture.width || tex.height != renderTexture.height)
{
tex.Reinitialize(renderTexture.width, renderTexture.height);
}
RenderTexture currentActiveRT = RenderTexture.active;
RenderTexture.active = renderTexture;
tex.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0);
tex.Apply();
renderTexture.Release();
RenderTexture.active = currentActiveRT;
Sprite thumbnailSprite = Sprite.Create(tex, new Rect(0f, 0f, tex.width, tex.height), new Vector2(0.5f, 0.5f), 100f);
EyeTrackerManager.Instance.Sprites.Add(thumbnailSprite);
thumbNailCreatedCallback?.Invoke(thumbnailSprite);
processInProgress = false;
if(thumbnailQueue.Count > 0)
{
GetThumbnailFromVideo(thumbnailQueue.Dequeue());
}
}
}
3 Likes
unity videoplayer can not jump to the specify frame immediately.
unity version: 2021.3.1f1c1
OS : window10 x64
OK ,Solved ,before call “player.frame=10” , we must pause the player by calling Pause function
then call prepare
public static Task FrameReadyAsync(this VideoPlayer videoPlayer, long frame)
{
var tcs = new TaskCompletionSource<bool>();
videoPlayer.audioOutputMode = VideoAudioOutputMode.None;// 不输出音频
videoPlayer.sendFrameReadyEvents = true;
videoPlayer.playOnAwake = false;
videoPlayer.Pause();
videoPlayer.frame = frame;
videoPlayer.Prepare();
videoPlayer.frameReady += (player, frameIdx) =>
{
UnityEngine.Debug.Log($"{nameof(VideoPlayerEx)}: frameIdx = {frameIdx} - {frame}");
if (frameIdx == frame)
{
tcs.TrySetResult(true);
}
};
return tcs.Task;
}