MediaPlayer_ExtractFrame.cs 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. using UnityEngine;
  2. using System.Collections;
  3. //-----------------------------------------------------------------------------
  4. // Copyright 2015-2022 RenderHeads Ltd. All rights reserved.
  5. //-----------------------------------------------------------------------------
  6. namespace RenderHeads.Media.AVProVideo
  7. {
  8. public partial class MediaPlayer : MonoBehaviour
  9. {
  10. #region Extract Frame
  11. private bool ForceWaitForNewFrame(int lastFrameCount, float timeoutMs)
  12. {
  13. bool result = false;
  14. // Wait for the frame to change, or timeout to happen (for the case that there is no new frame for this time)
  15. System.DateTime startTime = System.DateTime.Now;
  16. int iterationCount = 0;
  17. while (Control != null && (System.DateTime.Now - startTime).TotalMilliseconds < (double)timeoutMs)
  18. {
  19. _playerInterface.Update();
  20. // TODO: check if Seeking has completed! Then we don't have to wait
  21. // If frame has changed we can continue
  22. // NOTE: this will never happen because GL.IssuePlugin.Event is never called in this loop
  23. if (lastFrameCount != TextureProducer.GetTextureFrameCount())
  24. {
  25. result = true;
  26. break;
  27. }
  28. iterationCount++;
  29. // NOTE: we tried to add Sleep for 1ms but it was very slow, so switched to this time based method which burns more CPU but about double the speed
  30. // NOTE: had to add the Sleep back in as after too many iterations (over 1000000) of GL.IssuePluginEvent Unity seems to lock up
  31. // NOTE: seems that GL.IssuePluginEvent can't be called if we're stuck in a while loop and they just stack up
  32. //System.Threading.Thread.Sleep(0);
  33. }
  34. _playerInterface.Render();
  35. return result;
  36. }
  37. /// <summary>
  38. /// Create or return (if cached) a camera that is inactive and renders nothing
  39. /// This camera is used to call .Render() on which causes the render thread to run
  40. /// This is useful for forcing GL.IssuePluginEvent() to run and is used for
  41. /// wait for frames to render for ExtractFrame() and UpdateTimeScale()
  42. /// </summary>
  43. private static Camera GetDummyCamera()
  44. {
  45. if (_dummyCamera == null)
  46. {
  47. const string goName = "AVPro Video Dummy Camera";
  48. GameObject go = GameObject.Find(goName);
  49. if (go == null)
  50. {
  51. go = new GameObject(goName);
  52. go.hideFlags = HideFlags.HideInHierarchy | HideFlags.DontSave;
  53. go.SetActive(false);
  54. Object.DontDestroyOnLoad(go);
  55. _dummyCamera = go.AddComponent<Camera>();
  56. _dummyCamera.hideFlags = HideFlags.HideInInspector | HideFlags.DontSave;
  57. _dummyCamera.cullingMask = 0;
  58. _dummyCamera.clearFlags = CameraClearFlags.Nothing;
  59. _dummyCamera.enabled = false;
  60. }
  61. else
  62. {
  63. _dummyCamera = go.GetComponent<Camera>();
  64. }
  65. }
  66. //Debug.Assert(_dummyCamera != null);
  67. return _dummyCamera;
  68. }
  69. private IEnumerator ExtractFrameCoroutine(Texture2D target, ProcessExtractedFrame callback, double timeSeconds = -1.0, bool accurateSeek = true, int timeoutMs = 1000, int timeThresholdMs = 100)
  70. {
  71. #if (!UNITY_EDITOR && UNITY_ANDROID) || UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN || UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX || UNITY_IOS || UNITY_TVOS
  72. Texture2D result = target;
  73. Texture frame = null;
  74. if (_controlInterface != null)
  75. {
  76. if (timeSeconds >= 0f)
  77. {
  78. Pause();
  79. // If the right frame is already available (or close enough) just grab it
  80. if (TextureProducer.GetTexture() != null && (System.Math.Abs(_controlInterface.GetCurrentTime() - timeSeconds) < (timeThresholdMs / 1000.0)))
  81. {
  82. frame = TextureProducer.GetTexture();
  83. }
  84. else
  85. {
  86. int preSeekFrameCount = _textureInterface.GetTextureFrameCount();
  87. // Seek to the frame
  88. if (accurateSeek)
  89. {
  90. _controlInterface.Seek(timeSeconds);
  91. }
  92. else
  93. {
  94. _controlInterface.SeekFast(timeSeconds);
  95. }
  96. // Wait for the new frame to arrive
  97. if (!_controlInterface.WaitForNextFrame(GetDummyCamera(), preSeekFrameCount))
  98. {
  99. // If WaitForNextFrame fails (e.g. in android single threaded), we run the below code to asynchronously wait for the frame
  100. int currFc = TextureProducer.GetTextureFrameCount();
  101. int iterations = 0;
  102. int maxIterations = 50;
  103. //+1 as often there will be an extra frame produced after pause (so we need to wait for the second frame instead)
  104. while((currFc + 1) >= TextureProducer.GetTextureFrameCount() && iterations++ < maxIterations)
  105. {
  106. yield return null;
  107. }
  108. }
  109. frame = TextureProducer.GetTexture();
  110. }
  111. }
  112. else
  113. {
  114. frame = TextureProducer.GetTexture();
  115. }
  116. }
  117. if (frame != null)
  118. {
  119. result = Helper.GetReadableTexture(frame, TextureProducer.RequiresVerticalFlip(), Helper.GetOrientation(Info.GetTextureTransform()), target);
  120. }
  121. #else
  122. Texture2D result = ExtractFrame(target, timeSeconds, accurateSeek, timeoutMs, timeThresholdMs);
  123. #endif
  124. callback(result);
  125. yield return null;
  126. }
  127. public void ExtractFrameAsync(Texture2D target, ProcessExtractedFrame callback, double timeSeconds = -1.0, bool accurateSeek = true, int timeoutMs = 1000, int timeThresholdMs = 100)
  128. {
  129. StartCoroutine(ExtractFrameCoroutine(target, callback, timeSeconds, accurateSeek, timeoutMs, timeThresholdMs));
  130. }
  131. // "target" can be null or you can pass in an existing texture.
  132. public Texture2D ExtractFrame(Texture2D target, double timeSeconds = -1.0, bool accurateSeek = true, int timeoutMs = 1000, int timeThresholdMs = 100)
  133. {
  134. Texture2D result = target;
  135. // Extract frames returns the internal frame of the video player
  136. Texture frame = ExtractFrame(timeSeconds, accurateSeek, timeoutMs, timeThresholdMs);
  137. if (frame != null)
  138. {
  139. result = Helper.GetReadableTexture(frame, TextureProducer.RequiresVerticalFlip(), Helper.GetOrientation(Info.GetTextureTransform()), target);
  140. }
  141. return result;
  142. }
  143. private Texture ExtractFrame(double timeSeconds = -1.0, bool accurateSeek = true, int timeoutMs = 1000, int timeThresholdMs = 100)
  144. {
  145. Texture result = null;
  146. if (_controlInterface != null)
  147. {
  148. if (timeSeconds >= 0f)
  149. {
  150. Pause();
  151. // If the right frame is already available (or close enough) just grab it
  152. if (TextureProducer.GetTexture() != null && (System.Math.Abs(_controlInterface.GetCurrentTime() - timeSeconds) < (timeThresholdMs / 1000.0)))
  153. {
  154. result = TextureProducer.GetTexture();
  155. }
  156. else
  157. {
  158. // Store frame count before seek
  159. int frameCount = TextureProducer.GetTextureFrameCount();
  160. // Seek to the frame
  161. if (accurateSeek)
  162. {
  163. _controlInterface.Seek(timeSeconds);
  164. }
  165. else
  166. {
  167. _controlInterface.SeekFast(timeSeconds);
  168. }
  169. // Wait for frame to change
  170. ForceWaitForNewFrame(frameCount, timeoutMs);
  171. result = TextureProducer.GetTexture();
  172. }
  173. }
  174. else
  175. {
  176. result = TextureProducer.GetTexture();
  177. }
  178. }
  179. return result;
  180. }
  181. #endregion // Extract Frame
  182. }
  183. }