/// <summary> /// Resizes this player according to the predefined scale mode and the clip's aspect ratio. /// </summary> /// <param name="clip">Clip.</param> void Resize(AnimatedClip clip) { if (clip == null) { Debug.LogError("Could not resize player: clip is null."); return; } if (_scaleMode == ClipPlayerScaleMode.None) { return; } else { float aspectRatio = (float)clip.Width / clip.Height; if (_scaleMode == ClipPlayerScaleMode.AutoHeight) { rt.sizeDelta = new Vector2(rt.sizeDelta.x, rt.sizeDelta.x / aspectRatio); } else if (_scaleMode == ClipPlayerScaleMode.AutoWidth) { rt.sizeDelta = new Vector2(rt.sizeDelta.y * aspectRatio, rt.sizeDelta.y); } } }
IEnumerator CRPlay(AnimatedClip clip, float startDelay, bool loop) { float timePerFrame = 1f / clip.FramePerSecond; bool hasDelayed = false; do { for (int i = 0; i < clip.Frames.Length; i++) { rawImage.texture = clip.Frames[i]; yield return(new WaitForSeconds(timePerFrame)); // Wait at the 1st frame if startDelay is required if (startDelay > 0 && !hasDelayed && i == 0) { hasDelayed = true; yield return(new WaitForSeconds(startDelay)); } // Standby if paused if (isPaused) { yield return(null); } } } while (loop); }
private void DecodeProc(object param) { DecodeProcParams request = (DecodeProcParams)param; List <FrameData> frames = new List <FrameData>(); int width = 0; int height = 0; GifStream gifStream = new GifStream(request.filePath); int readFrame = 0; while (gifStream.HasMoreData) { if (readFrame >= request.frameToRead && request.frameToRead > 0) { break; } switch (gifStream.CurrentToken) { case GifStream.Token.Image: var image = gifStream.ReadImage(); width = gifStream.Header.width; height = gifStream.Header.height; Color32[] copiedColors = new Color32[image.colors.Length]; image.colors.CopyTo(copiedColors, 0); frames.Add(new FrameData() { colors = copiedColors, delay = image.DelaySeconds }); readFrame++; break; default: gifStream.SkipToken(); break; } } gifStream.Dispose(); request.runner.RunInMainThread(() => { Texture2D[] textures = new Texture2D[frames.Count]; int fps = 2; float totalTime = 0; for (int i = 0; i < frames.Count; i++) { textures[i] = new Texture2D(width, height, TextureFormat.ARGB32, false, false); textures[i].name = i.ToString(); textures[i].SetPixels32(frames[i].colors); textures[i].Apply(); totalTime += frames[i].delay; fps = Mathf.RoundToInt(frames.Count / totalTime); } animatedClip = new AnimatedClip(width, height, fps, textures); IsCompleted = true; request.runner.DestroySelf(); }); }
/// <summary> /// Plays the clip on the specified clip player. /// </summary> /// <param name="player">Player.</param> /// <param name="clip">Clip.</param> /// <param name="startDelay">Optional delay before the playing starts.</param> /// <param name="loop">If set to <c>true</c> loop indefinitely.</param> public static void PlayClip(IClipPlayer player, AnimatedClip clip, float startDelay = 0, bool loop = true) { if (player == null) { Debug.LogError("Player is null."); return; } else { player.Play(clip, startDelay, loop); } }
/// <summary> /// Stops recording on the specified recorder. /// </summary> /// <returns>The recorded clip.</returns> /// <param name="recorder">Recorder.</param> public static AnimatedClip StopRecording(Recorder recorder) { AnimatedClip clip = null; if (recorder == null) { Debug.LogError("StopRecording FAILED: recorder is null."); } else { clip = recorder.Stop(); } return(clip); }
/// <summary> /// Play the specified clip. /// </summary> /// <param name="clip">Clip.</param> /// <param name="startDelay">Optional delay before the playing starts.</param> /// <param name="loop">If set to <c>true</c> loop indefinitely.</param> public void Play(AnimatedClip clip, float startDelay = 0, bool loop = true) { if (clip == null || clip.Frames.Length == 0 || clip.IsDisposed()) { Debug.LogError("Attempted to play an empty or disposed clip."); return; } Stop(); Resize(clip); isPaused = false; playCoroutine = CRPlay(clip, startDelay, loop); StartCoroutine(playCoroutine); }
/// <summary> /// Exports a GIF image from the provided clip. /// Allows setting looping mode of the output image. /// </summary> /// <param name="clip">The clip to export to GIF format.</param> /// <param name="filename">Filename to save the output GIF.</param> /// <param name="loop">-1 to disable, 0 to loop indefinitely, >0 to loop a set number of times.</param> /// <param name="quality">Quality setting for the exported image. Inputs will be clamped between 1 and 100. Bigger values mean better quality but slightly longer processing time. 80 is generally a good value in terms of time-quality balance.</param> /// <param name="threadPriority">Priority of the GIF encoding thread.</param> /// <param name="exportProgressCallback">Export progress callback: 1st parameter is the provided clip, 2nd parameter is the export progress from 0 to 1.</param> /// <param name="exportCompletedCallback">Export completed callback: 1st parameter is the provided clip, 2nd parameter is the filepath of the exported GIF.</param> public static void ExportGif(AnimatedClip clip, string filename, int loop, int quality, System.Threading.ThreadPriority threadPriority, Action <AnimatedClip, float> exportProgressCallback, Action <AnimatedClip, string> exportCompletedCallback) { if (clip == null || clip.Frames.Length == 0 || clip.IsDisposed()) { Debug.LogError("Attempted to export GIF from an empty or disposed clip."); return; } if (String.IsNullOrEmpty(filename)) { Debug.LogError("Exporting GIF failed: filename is null or empty."); return; } // Start the actual export process Instance.StartCoroutine(CRExportGif(clip, filename, loop, quality, threadPriority, exportProgressCallback, exportCompletedCallback)); }
/// <summary> /// Stops recording. /// </summary> /// <returns>The recorded clip, which could be empty if the recording was not started.</returns> public AnimatedClip Stop() { _state = RecorderState.Stopped; if (recordedFrames.Count == 0) { Debug.LogWarning("Nothing recorded, an empty clip will be returned."); } var clip = new AnimatedClip(_width, _height, _framePerSecond, recordedFrames.ToArray()); // Since we return the clip, we also hand over the control of frame data to the holder of that clip // (e.g. the frames can be destroyed without our knowledge), therefore we should not hold any references to this data. recordedFrames.Clear(); return(clip); }
/// <summary> /// Resizes this player according to the predefined scale mode and the clip's aspect ratio. /// </summary> /// <param name="clip">Clip.</param> void Resize(AnimatedClip clip) { if (_scaleMode == ClipPlayerScaleMode.None) { return; } else { float aspectRatio = (float)clip.Width / clip.Height; var scale = transform.localScale; if (_scaleMode == ClipPlayerScaleMode.AutoHeight) { scale.y = scale.x / aspectRatio; } else if (_scaleMode == ClipPlayerScaleMode.AutoWidth) { scale.x = scale.y * aspectRatio; } transform.localScale = scale; } }
// GIF exporting coroutine: preprocess the image data then send it to native code (mobile) or a worker thread (other platforms) to export GIF file. static IEnumerator CRExportGif(AnimatedClip clip, string filename, int loop, int quality, System.Threading.ThreadPriority threadPriority, Action <AnimatedClip, float> exportProgressCallback, Action <AnimatedClip, string> exportCompletedCallback) { // The encoder don't want loop to be < -1 if (loop < -1) { loop = -1; } // Compute the NeuQuant sample factor from the inverse of the quality value. // Note that NeuQuant prefers values in range [1,30] so we'll also scale the factor to that range. int sampleFac = Mathf.RoundToInt(Mathf.Lerp(30, 1, (float)(Mathf.Clamp(quality, 1, 100)) / 100)); // Construct filepath string folder; #if UNITY_EDITOR folder = Application.dataPath; // Assets folder #else folder = Application.persistentDataPath; #endif string filepath = System.IO.Path.Combine(folder, filename + ".gif"); // Construct a new export task var exportTask = new GifExportTask(); exportTask.taskId = curExportId++; // assign this task a unique id exportTask.clip = clip; exportTask.imageData = null; exportTask.filepath = filepath; exportTask.loop = loop; exportTask.sampleFac = sampleFac; exportTask.exportProgressCallback = exportProgressCallback; exportTask.exportCompletedCallback = exportCompletedCallback; exportTask.workerPriority = threadPriority; exportTask.isExporting = true; exportTask.isDone = false; exportTask.progress = 0; // Add task to the list with its unique id key gifExportTasks.Add(exportTask.taskId, exportTask); yield return(null); // Create a temporary texture to read RenderTexture data Texture2D temp = new Texture2D(clip.Width, clip.Height, TextureFormat.RGB24, false); temp.hideFlags = HideFlags.HideAndDontSave; temp.wrapMode = TextureWrapMode.Clamp; temp.filterMode = FilterMode.Bilinear; temp.anisoLevel = 0; // On iOS and Android, the GIF encoding is done in native code. // In Unity editor (and other platforms), we use Moments encoder for testing purpose. #if UNITY_EDITOR || (!UNITY_IOS && !UNITY_ANDROID) // Converts to GIF frames List <GifFrame> frames = new List <GifFrame>(clip.Frames.Length); for (int i = 0; i < clip.Frames.Length; i++) { RenderTexture source = clip.Frames[i]; RenderTexture.active = source; temp.ReadPixels(new Rect(0, 0, source.width, source.height), 0, 0); temp.Apply(); RenderTexture.active = null; GifFrame frame = new GifFrame() { Width = temp.width, Height = temp.height, Data = temp.GetPixels32() }; frames.Add(frame); OnGifPreProcessing(exportTask.taskId, (float)i / clip.Frames.Length); yield return(null); } // Setup a worker thread and let it do its magic GifEncoder encoder = new GifEncoder(loop, sampleFac); encoder.SetDelay(Mathf.RoundToInt(1000f / clip.FramePerSecond)); Worker worker = new Worker( exportTask.taskId, threadPriority, frames, encoder, filepath, OnGifExportProgress, OnGifExportCompleted); worker.Start(); #else // Allocate an array to hold the serialized image data exportTask.imageData = new Color32[clip.Frames.Length][]; // Construct the serialized image data, note that texture data is layered down-top, so flip it for (int i = 0; i < clip.Frames.Length; i++) { var source = clip.Frames[i]; RenderTexture.active = source; temp.ReadPixels(new Rect(0, 0, source.width, source.height), 0, 0); temp.Apply(); RenderTexture.active = null; // Get the frame's pixel data exportTask.imageData[i] = temp.GetPixels32(); // Call the preprocessing handler directly float progress = (float)i / clip.Frames.Length; OnGifPreProcessing(exportTask.taskId, progress); yield return(null); } #if UNITY_IOS iOSNativeGif.ExportGif(exportTask); #elif UNITY_ANDROID AndroidNativeGif.ExportGif(exportTask); #endif #endif // UNITY_EDITOR || (!UNITY_IOS && !UNITY_ANDROID) // Dispose the temporary texture Destroy(temp); }
/// <summary> /// Exports a GIF image from the provided clip. /// </summary> /// <param name="clip">The clip to export to GIF format.</param> /// <param name="filename">Filename to save the output GIF.</param> /// <param name="quality">Quality setting for the exported image. Inputs will be clamped between 1 and 100. Bigger values mean better quality but slightly longer processing time. 80 is generally a good value in terms of time-quality balance.</param> /// <param name="threadPriority">Thread priority to use when exporting GIF file.</param> /// <param name="exportProgressCallback">Export progress callback: 1st parameter is the provided clip, 2nd parameter is the export progress from 0 to 1.</param> /// <param name="exportCompletedCallback">Export completed callback: 1st parameter is the provided clip, 2nd parameter is the filepath of the exported GIF.</param> public static void ExportGif(AnimatedClip clip, string filename, int quality, System.Threading.ThreadPriority threadPriority, Action <AnimatedClip, float> exportProgressCallback, Action <AnimatedClip, string> exportCompletedCallback) { ExportGif(clip, filename, 0, quality, threadPriority, exportProgressCallback, exportCompletedCallback); }