//----------------------------------------------------------------------------- // Copyright 2017 Old Moat Games. All rights reserved. //----------------------------------------------------------------------------- using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading; using UnityEngine; using UnityEngine.Networking; using UnityEngine.UI; namespace OldMoatGames { public enum GifPlayerState { PreProcessing, Loading, Stopped, Playing, Error, Disabled } public enum GifPath { StreamingAssetsPath, PersistentDataPath, TemporaryCachePath } [AddComponentMenu("Miscellaneous/Animated GIF Player")] public class AnimatedGifPlayer : MonoBehaviour { #region Public Fields /// /// Sets the GIF to continue playing from the start after it is finished. /// public bool Loop = true; /// /// Sets the GIF to automatically start playing after it is loaded. /// public bool AutoPlay = true; /// /// File used for GIF. Use GifPath to set the base path to StreamingAssetsPath, PersistentDataPath or /// TemporaryCachePath. /// public string FileName = "http://i.imgur.com/3vW7Vk6.gif"; //"http://i.imgur.com/Jg0gWBo.gif"; /// /// Path to the GIF. /// public GifPath Path = GifPath.StreamingAssetsPath; /// /// Returns the width of the GIF /// public int Width => _gifDecoder == null ? 0 : _gifDecoder.GetFrameWidth(); #pragma warning disable 414 private Material originalMaterial = null; #pragma warning restore 414 /// /// Returns the heigth of the GIF /// public int Height => _gifDecoder == null ? 0 : _gifDecoder.GetFrameHeight(); /// /// Sets caching for decoded frames. Reload the GIF for it to take effect. /// public bool CacheFrames; /// /// Sets buffering for frames. When set all frames are loaded and cached at once. Reload GIF for it to take effect. /// public bool BufferAllFrames; // Buffer all frames after awake /// /// Sets whether or not the decoder runs in a separate thread /// public bool UseThreadedDecoder = false; /// /// Sets whether or not to run the player in compatibility mode. /// This mode supports more decoding methods but uses more memory and CPU. /// public bool CompatibilityMode; /// /// Sets whether or not playback speed is independent of Time.timeScale. /// public bool OverrideTimeScale; /// /// Sets time scale of Gif playback. /// public float TimeScale = 1; /// /// The current target component for the GIF. Reload GIF for it to take effect. /// public Component TargetComponent { get => _targetComponent; set => _targetComponent = value; } /// /// The current target material in the component. Used when there is more than 1 material /// public int TargetMaterialNumber { get => _targetMaterial; set => _targetMaterial = value; } /// /// Current state of the GIF player. /// public GifPlayerState State { get; private set; } /// /// Texture on which the frame is displayed. Texture should be a RGBA32 texture of the same piel size as the GIF. If no /// texture specified a new one will be created /// public Texture2D GifTexture; #endregion #region Delegates /// /// Called when GIF is ready to play. /// public delegate void OnReadyAction(); public event OnReadyAction OnReady; /// /// Called when GIF could not be loaded. /// public delegate void OnLoadErrorAction(); #pragma warning disable 67 public event OnLoadErrorAction OnLoadError; #pragma warning restore 67 #endregion #region Internal fields private GifDecoder _gifDecoder; // The GIF decoder private bool _hasFirstFrameBeenShown; // Has the first frame of the GIF already been shown [SerializeField] private Component _targetComponent; // Target component [SerializeField] private int _targetMaterial; // Target material number private bool _cacheFrames; private bool _bufferAllFrames; private bool _useThreadedDecoder; private float _secondsTillNextFrame; // Seconds till next frame private List _cachedFrames; // Cache of all frames that have been decoded private GifDecoder.GifFrame CurrentFrame { get; set; } // The current frame that is being displayed private int CurrentFrameNumber { get; set; } // The current frame we are at private Thread _decodeThread; private readonly EventWaitHandle _wh = new AutoResetEvent(false); private bool _threadIsCanceled; private bool _frameIsReady; private bool _threadLoop; private bool _threadBufferAll; private readonly object _locker = new object(); private float _editorPreviousUpdateTime; // Time of previous update in editor #endregion #region Unity Events private void Awake() { //init the AnimatedGifPlayer // if (State == GifPlayerState.PreProcessing) Init(); } public void Update() { //check if we need to update the gif frame CheckFrameChange(); } private void OnApplicationQuit() { EndDecodeThread(); } #endregion #region Public API /// /// Initializes the component with callbacks that are triggered when loading has finished or has failed. /// public void Init() { #if UNITY_EDITOR if (!Application.isPlaying) { _cacheFrames = _bufferAllFrames = true; // Buffer and cache all frames in the editor _useThreadedDecoder = false; // Don't use the threaded decoder in the editor } else { _cacheFrames = CacheFrames; _bufferAllFrames = BufferAllFrames; _useThreadedDecoder = UseThreadedDecoder; } #else _cacheFrames = CacheFrames; _bufferAllFrames = BufferAllFrames; _useThreadedDecoder = UseThreadedDecoder; #endif #if UNITY_WEBGL if (_useThreadedDecoder) { Debug.LogWarning("Animated GIF Player: Threaded Decoder is not available in WebGL"); _useThreadedDecoder = false; } #endif #if UNITY_WSA if (_useThreadedDecoder) { Debug.LogWarning("Animated GIF Player: Threaded Decoder is not available in Universal Windows Platform"); _useThreadedDecoder = false; } #endif if (_bufferAllFrames && !_cacheFrames) // Don't buffer frames if they are not cached _bufferAllFrames = false; if (_cacheFrames) //init the cache _cachedFrames = new List(); // Store the target component _targetComponent = GetTargetComponent(); // Start new decoder _gifDecoder = new GifDecoder(CompatibilityMode); CurrentFrameNumber = 0; _hasFirstFrameBeenShown = false; _frameIsReady = false; // Set state to disabled State = GifPlayerState.Disabled; // Start the decoder thread StartDecodeThread(); if (FileName.Length <= 0) return; // Only load if the file name is set #if UNITY_EDITOR if (Application.isPlaying) { StartCoroutine(Load()); } else { // Do not use a coroutine in the editor var e = Load(); while (e.MoveNext()) { } } #else StartCoroutine(Load()); #endif } /// /// Start playback. /// public void Play() { if (State != GifPlayerState.Stopped) { Debug.LogWarning("Can't play GIF playback. State is: " + State); return; } State = GifPlayerState.Playing; } /// /// Pause playback. /// public void Pause() { if (State != GifPlayerState.Playing) // Debug.LogWarning("Can't pause GIF is not playing. State is: " + State); return; State = GifPlayerState.Stopped; } /// /// Returns the number of frames in the GIF. Only shows the number of frames that have been decoded. /// /// /// Number of frames /// public int GetNumberOfFrames() { return _gifDecoder == null ? 0 : _gifDecoder.GetFrameCount(); } #endregion #region Methods /// /// Start loading the GIF. /// private IEnumerator Load() { if (FileName.Length == 0) // Debug.LogWarning("File name not set"); yield break; // Set status to loading State = GifPlayerState.Loading; string path; // Create file path if (FileName.Substring(0, 4) == "http") { // Get image from web path = FileName; } else { // Local storage string gifPath; switch (Path) { case GifPath.StreamingAssetsPath: gifPath = Application.streamingAssetsPath; break; case GifPath.PersistentDataPath: gifPath = Application.persistentDataPath; break; case GifPath.TemporaryCachePath: gifPath = Application.temporaryCachePath; break; default: gifPath = Application.streamingAssetsPath; break; } #if !UNITY_2017_2_OR_NEWER // Url encode for Unity 2017 1 or newer path = Uri.EscapeUriString(FileName); // Encode # path = path.Replace("#", "%23"); #endif #if (UNITY_ANDROID && !UNITY_EDITOR) || (UNITY_WEBGL && !UNITY_EDITOR) path = System.IO.Path.Combine(gifPath, FileName); #else path = System.IO.Path.Combine("file://" + gifPath, FileName); #endif } #pragma warning disable 618 using (var www = new WWW(path.Replace("#", "%23"))) #pragma warning restore 618 { yield return www; if (string.IsNullOrEmpty(www.error) == false) { // Gif file could not be loaded from streaming assets Debug.LogWarning("File load error.\n" + www.error + "\nPath:" + WWW.EscapeURL(path, Encoding.Default)); State = GifPlayerState.Error; } else // Gif file loaded. Pass stream to gif decoder lock (_locker) { var stream = new MemoryStream(www.bytes); if (_gifDecoder.Read(stream) == GifDecoder.Status.StatusOk) { // Gif header was read. Prepare the gif // Set status to preprocessing State = GifPlayerState.PreProcessing; // Create the target texture CreateTargetTexture(); // Show the first frame StartDecoder(); } else { // Maybe image? StartCoroutine(DownloadImage(path)); // Error decoding gif //Debug.LogWarning("Error loading gif"); //State = GifPlayerState.Error; //if (OnLoadError != null) OnLoadError(); } } } } private IEnumerator DownloadImage(string MediaUrl) { // // // GetTextures() is broken on the version of unity currently used. var texture2 = new Texture2D(1, 1); var www = new WWW(MediaUrl); yield return www; www.LoadImageIntoTexture(texture2); GifTexture = texture2; if (GifTexture == null) yield break; GifTexture.hideFlags = HideFlags.HideAndDontSave; SetTexture(); yield break; // // var request = UnityWebRequest.Get(MediaUrl); var request = UnityWebRequestTexture.GetTexture(MediaUrl); yield return request.SendWebRequest(); #pragma warning disable 618 if (request.isNetworkError || request.isHttpError) #pragma warning restore 618 { Debug.Log(request.error); } else { Debug.Log("Download Handler"); Debug.Log(request.downloadHandler); if (request.downloadHandler is not DownloadHandlerTexture texture) { yield break; } // var texture = request.downloadHandler.data; GifTexture = texture.texture; if (GifTexture == null) yield break; GifTexture.hideFlags = HideFlags.HideAndDontSave; SetTexture(); } } // Create target texture private void CreateTargetTexture() { if (GifTexture != null && _gifDecoder != null && GifTexture.width == _gifDecoder.GetFrameWidth() && GifTexture.height == _gifDecoder.GetFrameHeight()) return; // Target texture already set if (_gifDecoder == null || _gifDecoder.GetFrameWidth() == 0 || _gifDecoder.GetFrameWidth() == 0) { GifTexture = Texture2D.blackTexture; return; } if (GifTexture != null && GifTexture.hideFlags == HideFlags.HideAndDontSave) DestroyImmediate(GifTexture); GifTexture = CreateTexture(_gifDecoder.GetFrameWidth(), _gifDecoder.GetFrameHeight()); GifTexture.hideFlags = HideFlags.HideAndDontSave; } // Used to determine the target component if not set public void SetTexture() { if (_targetComponent == null) return; // SpriteRenderer if (_targetComponent is SpriteRenderer) { var target = (SpriteRenderer)_targetComponent; #if UNITY_5_6_OR_NEWER var oldSize = target.size; #endif var newSprite = Sprite.Create(GifTexture, new Rect(0.0f, 0.0f, GifTexture.width, GifTexture.height), new Vector2(0.5f, 0.5f), 100f, 0, SpriteMeshType.FullRect); newSprite.name = "Gif Player Sprite"; newSprite.hideFlags = HideFlags.HideAndDontSave; target.sprite = newSprite; #if UNITY_5_6_OR_NEWER target.size = oldSize; #endif return; } // Renderer if (_targetComponent is Renderer) { var target = (Renderer)_targetComponent; Material newMat = new Material(SCoreModEvents.GetStandardShader()); target.material = newMat; newMat.mainTexture = GifTexture; return; } // RawImage if (_targetComponent is RawImage) { var target = (RawImage)_targetComponent; target.texture = GifTexture; } } // Returns a Renderer or RawImage component //private Component GetTargetComponent() //{ // var components = GetComponents(); // return components.FirstOrDefault(component => component is Renderer || component is RawImage); //} private Component GetTargetComponent() { var components = GetComponentsInChildren(); foreach (var component in components.Where(a => a is Renderer)) return component; return null; } // Used to set the frame target in the target component private void SetTargetTexture() { if (GifTexture == null || GifTexture.width != _gifDecoder.GetFrameWidth() || GifTexture.height != _gifDecoder.GetFrameWidth()) GifTexture = CreateTexture(_gifDecoder.GetFrameWidth(), _gifDecoder.GetFrameHeight()); GifTexture.hideFlags = HideFlags.HideAndDontSave; if (TargetComponent == null) return; if (TargetComponent is MeshRenderer) { var target = (Renderer)TargetComponent; if (target.sharedMaterial == null) return; //Material newMat = new Material(target.sharedMaterial.shader); //newMat.mainTexture = GifTexture; //target.material = newMat; if (target.sharedMaterials.Length > 0 && target.sharedMaterials.Length > _targetMaterial) target.sharedMaterials[_targetMaterial].mainTexture = GifTexture; else target.sharedMaterial.mainTexture = GifTexture; } if (TargetComponent is SpriteRenderer) { var target = (SpriteRenderer)TargetComponent; var newSprite = Sprite.Create(GifTexture, new Rect(0.0f, 0.0f, GifTexture.width, GifTexture.height), new Vector2(0.5f, 0.5f)); newSprite.name = "Gif Player Sprite"; newSprite.hideFlags = HideFlags.HideAndDontSave; target.sprite = newSprite; } if (TargetComponent is RawImage) { var target = (RawImage)TargetComponent; target.texture = GifTexture; } } // Creates the texture used private static Texture2D CreateTexture(int width, int height) { return new Texture2D(width, height, TextureFormat.RGBA32, false); } // Reads and caches all frames private void BufferFrames() { if (_useThreadedDecoder) { // Threaded _wh.Set(); // Signal thread to read the next frame return; } // Not threaded lock (_locker) { while (true) { // Read a single frame _gifDecoder.ReadNextFrame(false); if (_gifDecoder.AllFramesRead) break; // Get the current frame var frame = _gifDecoder.GetCurrentFrame(); // Add frame to frame cache AddFrameToCache(frame); } _frameIsReady = true; } } // Add a frame to the frame cache private void AddFrameToCache(GifDecoder.GifFrame frame) { // Create a coopy of the data to add to the cache since the frame data array is reused for the next frame var copyOfImage = new byte[frame.Image.Length]; Buffer.BlockCopy(frame.Image, 0, copyOfImage, 0, frame.Image.Length); frame.Image = copyOfImage; // Add frame to frame list lock (_cachedFrames) { _cachedFrames.Add(frame); } } // Shows the first frame private void StartDecoder() { if (_bufferAllFrames) // Buffer all frames BufferFrames(); else // Prepare the next frame StartReadFrame(); //player is ready to start State = GifPlayerState.Stopped; //the ready event if (OnReady != null) OnReady(); #if UNITY_EDITOR if (AutoPlay && Application.isPlaying) Play(); //don't start autoplay in the editor #else if (AutoPlay) Play(); #endif } // Sets the time at which the next frame should be shown private void SetNextFrameTime() { _secondsTillNextFrame = CurrentFrame.Delay; } // Check if the next frame should be shown private void UpdateFrameTime() { if (State != GifPlayerState.Playing) return; // Not playing if (!Application.isPlaying || OverrideTimeScale) { // Play in editor mode or time is independant from Time.timeScale if (OverrideTimeScale) _secondsTillNextFrame -= (Time.realtimeSinceStartup - _editorPreviousUpdateTime) * TimeScale; else _secondsTillNextFrame -= Time.realtimeSinceStartup - _editorPreviousUpdateTime; _editorPreviousUpdateTime = Time.realtimeSinceStartup; return; } // Calculate seconds till next gif frame _secondsTillNextFrame -= Time.deltaTime; } // Update the frame private void UpdateFrame() { if (_gifDecoder.NumberOfFrames > 0 && _gifDecoder.NumberOfFrames == CurrentFrameNumber) { // Set frame number to 0 if we are at the last one CurrentFrameNumber = 0; if (!Loop) { // Stop playback if not looping Pause(); return; } } if (_cacheFrames) { // Frames are cached lock (_cachedFrames) { CurrentFrame = _cachedFrames.Count > CurrentFrameNumber ? _cachedFrames[CurrentFrameNumber] : _gifDecoder.GetCurrentFrame(); } // Prepare the next frame if (!_gifDecoder.AllFramesRead) // Not all frames are read yet. Prepare the next frame StartReadFrame(); } else { // Get the frame from the decoder CurrentFrame = _gifDecoder.GetCurrentFrame(); } // Update the target texture with the new frame UpdateTexture(); // Set next frame time SetNextFrameTime(); // Move to next frame CurrentFrameNumber++; if (!_cacheFrames) StartReadFrame(); // Prepare the next frame } // Check if the frame needs to be updated private void CheckFrameChange() { if (State != GifPlayerState.Playing && _hasFirstFrameBeenShown || !_frameIsReady) return; if (State == GifPlayerState.Loading) return; if (!_hasFirstFrameBeenShown) { // Show the first frame SetTexture(); lock (_locker) { UpdateFrame(); } _hasFirstFrameBeenShown = true; return; } UpdateFrameTime(); if (_secondsTillNextFrame > 0) return; // Time to change the frame lock (_locker) { UpdateFrame(); } } // Update the target texture private void UpdateTexture() { if (CurrentFrame?.Image == null) return; // Upload texture data GifTexture.LoadRawTextureData(CurrentFrame.Image); // Apply GifTexture.Apply(); } // Starts reading a frame private void StartReadFrame() { _frameIsReady = false; if (_useThreadedDecoder) { // Signal thread to read the next frame _wh.Set(); return; } // Not threaded if (_cacheFrames && _gifDecoder.AllFramesRead) return; // Don't retrieve data if we already have cached all frames // Read the next frame _gifDecoder.ReadNextFrame(!_cacheFrames); // Add frame to cache if caching is enabled if (_cacheFrames && !_gifDecoder.AllFramesRead) AddFrameToCache(_gifDecoder.GetCurrentFrame()); // Mark the frame as ready _frameIsReady = true; } // Start the decode thread private void StartDecodeThread() { #if !UNITY_WSA if (!_useThreadedDecoder) return; lock (_locker) { _threadLoop = !_cacheFrames; _threadBufferAll = _bufferAllFrames; } if (_decodeThread != null) return; // Thread is already running _threadIsCanceled = false; _decodeThread = new Thread(FrameDataThread); _decodeThread.Name = "gifDecoder" + _decodeThread.ManagedThreadId; _decodeThread.IsBackground = true; _decodeThread.Start(); #endif } // Ends the decode thread. Used to clean up after application quit private void EndDecodeThread() { if (_threadIsCanceled) return; _threadIsCanceled = true; _wh.Set(); } // The decode thread private void FrameDataThread() { _wh.WaitOne(); while (!_threadIsCanceled) { lock (_locker) { // Read the next frame _gifDecoder.ReadNextFrame(_threadLoop); if (_cacheFrames && _gifDecoder.AllFramesRead) { _frameIsReady = true; break; } if (_cacheFrames) AddFrameToCache(_gifDecoder.GetCurrentFrame()); if (_threadBufferAll) { if (_gifDecoder.AllFramesRead) { _frameIsReady = true; break; } continue; } _frameIsReady = true; } _wh.WaitOne(); // Wait for signal that frame must be read } _threadIsCanceled = true; _decodeThread = null; } #endregion } }