Helper.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. using UnityEngine;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. //-----------------------------------------------------------------------------
  5. // Copyright 2015-2024 RenderHeads Ltd. All rights reserved.
  6. //-----------------------------------------------------------------------------
  7. namespace RenderHeads.Media.AVProVideo
  8. {
  9. public static class Helper
  10. {
  11. public const string AVProVideoVersion = "3.0.6";
  12. public sealed class ExpectedPluginVersion
  13. {
  14. public const string Windows = "3.0.5";
  15. public const string WinRT = "3.0.5";
  16. public const string Android = "3.0.6";
  17. public const string Apple = "3.0.5";
  18. }
  19. public const string UnityBaseTextureName = "_MainTex";
  20. public const string UnityBaseTextureName_URP = "_BaseMap";
  21. public const string UnityBaseTextureName_HDRP = "_BaseColorMap";
  22. public static string GetPath(MediaPathType location)
  23. {
  24. string result = string.Empty;
  25. switch (location)
  26. {
  27. case MediaPathType.AbsolutePathOrURL:
  28. break;
  29. case MediaPathType.RelativeToDataFolder:
  30. result = Application.dataPath;
  31. break;
  32. case MediaPathType.RelativeToPersistentDataFolder:
  33. result = Application.persistentDataPath;
  34. break;
  35. case MediaPathType.RelativeToProjectFolder:
  36. #if !UNITY_WINRT_8_1
  37. string path = "..";
  38. #if UNITY_STANDALONE_OSX && !UNITY_EDITOR_OSX
  39. path += "/..";
  40. #endif
  41. result = System.IO.Path.GetFullPath(System.IO.Path.Combine(Application.dataPath, path));
  42. result = result.Replace('\\', '/');
  43. #endif
  44. break;
  45. case MediaPathType.RelativeToStreamingAssetsFolder:
  46. result = Application.streamingAssetsPath;
  47. break;
  48. }
  49. return result;
  50. }
  51. public static string GetFilePath(string path, MediaPathType location)
  52. {
  53. string result = string.Empty;
  54. if (!string.IsNullOrEmpty(path))
  55. {
  56. switch (location)
  57. {
  58. case MediaPathType.AbsolutePathOrURL:
  59. result = path;
  60. break;
  61. case MediaPathType.RelativeToDataFolder:
  62. case MediaPathType.RelativeToPersistentDataFolder:
  63. case MediaPathType.RelativeToProjectFolder:
  64. case MediaPathType.RelativeToStreamingAssetsFolder:
  65. result = System.IO.Path.Combine(GetPath(location), path);
  66. break;
  67. }
  68. }
  69. return result;
  70. }
  71. public static string GetFriendlyResolutionName(int width, int height, float fps)
  72. {
  73. // List of common 16:9 resolutions
  74. int[] areas = { 0, 7680 * 4320, 3840 * 2160, 2560 * 1440, 1920 * 1080, 1280 * 720, 853 * 480, 640 * 360, 426 * 240, 256 * 144 };
  75. string[] names = { "Unknown", "8K", "4K", "1440p", "1080p", "720p", "480p", "360p", "240p", "144p" };
  76. Debug.Assert(areas.Length == names.Length);
  77. // Find the closest resolution
  78. int closestAreaIndex = 0;
  79. int area = width * height;
  80. int minDelta = int.MaxValue;
  81. for (int i = 0; i < areas.Length; i++)
  82. {
  83. int d = Mathf.Abs(areas[i] - area);
  84. // TODO: add a maximum threshold to ignore differences that are too high
  85. if (d < minDelta)
  86. {
  87. closestAreaIndex = i;
  88. minDelta = d;
  89. // If the exact mode is found, early out
  90. if (d == 0)
  91. {
  92. break;
  93. }
  94. }
  95. }
  96. string result = names[closestAreaIndex];
  97. // Append frame rate if valid
  98. if (fps > 0f && !float.IsNaN(fps))
  99. {
  100. result += fps.ToString("0.##");
  101. }
  102. return result;
  103. }
  104. public static string GetErrorMessage(ErrorCode code)
  105. {
  106. string result = string.Empty;
  107. switch (code)
  108. {
  109. case ErrorCode.None:
  110. result = "No Error";
  111. break;
  112. case ErrorCode.LoadFailed:
  113. result = "Loading failed. File not found, codec not supported, video resolution too high or insufficient system resources.";
  114. #if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
  115. // Add extra information for older Windows versions that don't have support for modern codecs
  116. if (SystemInfo.operatingSystem.StartsWith("Windows XP") ||
  117. SystemInfo.operatingSystem.StartsWith("Windows Vista"))
  118. {
  119. result += " NOTE: Windows XP and Vista don't have native support for H.264 codec. Consider using an older codec such as DivX or installing 3rd party codecs such as LAV Filters.";
  120. }
  121. #endif
  122. break;
  123. case ErrorCode.DecodeFailed:
  124. result = "Decode failed. Possible codec not supported, video resolution/bit-depth too high, or insufficient system resources.";
  125. #if UNITY_ANDROID
  126. result += " On Android this is generally due to the hardware not having enough resources to decode the video. Most Android devices can only handle a maximum of one 4K video at once.";
  127. #endif
  128. break;
  129. }
  130. return result;
  131. }
  132. public static string GetPlatformName(Platform platform)
  133. {
  134. string result = "Unknown";
  135. switch (platform)
  136. {
  137. case Platform.WindowsUWP:
  138. result = "Windows UWP";
  139. break;
  140. default:
  141. result = platform.ToString();
  142. break;
  143. }
  144. return result;
  145. }
  146. public static string[] GetPlatformNames()
  147. {
  148. return new string[] {
  149. GetPlatformName(Platform.Windows),
  150. GetPlatformName(Platform.macOS),
  151. GetPlatformName(Platform.iOS),
  152. GetPlatformName(Platform.tvOS),
  153. GetPlatformName(Platform.visionOS),
  154. GetPlatformName(Platform.Android),
  155. GetPlatformName(Platform.WindowsUWP),
  156. GetPlatformName(Platform.WebGL),
  157. };
  158. }
  159. #if AVPROVIDEO_DISABLE_LOGGING
  160. [System.Diagnostics.Conditional("ALWAYS_FALSE")]
  161. #endif
  162. public static void LogInfo(string message, Object context = null)
  163. {
  164. if (context == null)
  165. {
  166. Debug.Log("[AVProVideo] " + message);
  167. }
  168. else
  169. {
  170. Debug.Log("[AVProVideo] " + message, context);
  171. }
  172. }
  173. public static int GetUnityAudioSampleRate()
  174. {
  175. // For standalone builds (not in the editor):
  176. // In Unity 4.6, 5.0, 5.1 when audio is disabled there is no indication from the API.
  177. // But in 5.2.0 and above, it logs an error when trying to call
  178. // AudioSettings.GetDSPBufferSize() or AudioSettings.outputSampleRate
  179. // So to prevent the error, check if AudioSettings.GetConfiguration().sampleRate == 0
  180. return (AudioSettings.GetConfiguration().sampleRate == 0) ? 0 : AudioSettings.outputSampleRate;
  181. }
  182. public static int GetUnityAudioSpeakerCount()
  183. {
  184. switch (AudioSettings.GetConfiguration().speakerMode)
  185. {
  186. case AudioSpeakerMode.Mono: return 1;
  187. case AudioSpeakerMode.Stereo: return 2;
  188. case AudioSpeakerMode.Quad: return 4;
  189. case AudioSpeakerMode.Surround: return 5;
  190. case AudioSpeakerMode.Mode5point1: return 6;
  191. case AudioSpeakerMode.Mode7point1: return 8;
  192. case AudioSpeakerMode.Prologic: return 2;
  193. }
  194. return 0;
  195. }
  196. // Returns a valid range to use for a timeline display
  197. // Either it will return the range 0..duration, or
  198. // for live streams it will return first seekable..last seekable time
  199. public static TimeRange GetTimelineRange(double duration, TimeRanges seekable)
  200. {
  201. TimeRange result = new TimeRange();
  202. if (duration >= 0.0 && duration < 2e10)
  203. {
  204. // Duration is valid
  205. result.startTime = 0f;
  206. result.duration = duration;
  207. }
  208. else
  209. {
  210. // Duration is invalid, so it could be a live stream, so derive from seekable range
  211. result.startTime = seekable.MinTime;
  212. result.duration = seekable.Duration;
  213. }
  214. return result;
  215. }
  216. public const double SecondsToHNS = 10000000.0;
  217. public const double MilliSecondsToHNS = 10000.0;
  218. public static string GetTimeString(double timeSeconds, bool showMilliseconds = false)
  219. {
  220. float totalSeconds = (float)timeSeconds;
  221. int hours = Mathf.FloorToInt(totalSeconds / (60f * 60f));
  222. float usedSeconds = hours * 60f * 60f;
  223. int minutes = Mathf.FloorToInt((totalSeconds - usedSeconds) / 60f);
  224. usedSeconds += minutes * 60f;
  225. int seconds = Mathf.FloorToInt(totalSeconds - usedSeconds);
  226. string result;
  227. if (hours <= 0)
  228. {
  229. if (showMilliseconds)
  230. {
  231. int milliSeconds = (int)((totalSeconds - Mathf.Floor(totalSeconds)) * 1000f);
  232. result = string.Format("{0:00}:{1:00}:{2:000}", minutes, seconds, milliSeconds);
  233. }
  234. else
  235. {
  236. result = string.Format("{0:00}:{1:00}", minutes, seconds);
  237. }
  238. }
  239. else
  240. {
  241. if (showMilliseconds)
  242. {
  243. int milliSeconds = (int)((totalSeconds - Mathf.Floor(totalSeconds)) * 1000f);
  244. result = string.Format("{2}:{0:00}:{1:00}:{3:000}", minutes, seconds, hours, milliSeconds);
  245. }
  246. else
  247. {
  248. result = string.Format("{2}:{0:00}:{1:00}", minutes, seconds, hours);
  249. }
  250. }
  251. return result;
  252. }
  253. /// <summary>
  254. /// Convert texture transform matrix to an enum of orientation types
  255. /// </summary>
  256. public static Orientation GetOrientation(float[] t)
  257. {
  258. Orientation result = Orientation.Landscape;
  259. if (t != null)
  260. {
  261. // TODO: check that the Portrait and PortraitFlipped are the right way around
  262. if (t[0] == 0f && t[1]== 1f && t[2] == -1f && t[3] == 0f)
  263. {
  264. result = Orientation.Portrait;
  265. } else
  266. if (t[0] == 0f && t[1] == -1f && t[2] == 1f && t[3] == 0f)
  267. {
  268. result = Orientation.PortraitFlipped;
  269. } else
  270. if (t[0]== 1f && t[1] == 0f && t[2] == 0f && t[3] == 1f)
  271. {
  272. result = Orientation.Landscape;
  273. } else
  274. if (t[0] == -1f && t[1] == 0f && t[2] == 0f && t[3] == -1f)
  275. {
  276. result = Orientation.LandscapeFlipped;
  277. }
  278. else
  279. if (t[0] == 0f && t[1] == 1f && t[2] == 1f && t[3] == 0f)
  280. {
  281. result = Orientation.PortraitHorizontalMirror;
  282. }
  283. }
  284. return result;
  285. }
  286. private static Matrix4x4 PortraitMatrix = Matrix4x4.TRS(new Vector3(0f, 1f, 0f), Quaternion.Euler(0f, 0f, -90f), Vector3.one);
  287. private static Matrix4x4 PortraitFlippedMatrix = Matrix4x4.TRS(new Vector3(1f, 0f, 0f), Quaternion.Euler(0f, 0f, 90f), Vector3.one);
  288. private static Matrix4x4 LandscapeFlippedMatrix = Matrix4x4.TRS(new Vector3(1f, 1f, 0f), Quaternion.Euler(0f, 0f, -180f), Vector3.one);
  289. public static Matrix4x4 GetMatrixForOrientation(Orientation ori)
  290. {
  291. Matrix4x4 result;
  292. switch (ori)
  293. {
  294. case Orientation.Landscape:
  295. result = Matrix4x4.identity;
  296. break;
  297. case Orientation.LandscapeFlipped:
  298. result = LandscapeFlippedMatrix;
  299. break;
  300. case Orientation.Portrait:
  301. result = PortraitMatrix;
  302. break;
  303. case Orientation.PortraitFlipped:
  304. result = PortraitFlippedMatrix;
  305. break;
  306. case Orientation.PortraitHorizontalMirror:
  307. result = new Matrix4x4();
  308. result.SetColumn(0, new Vector4(0f, 1f, 0f, 0f));
  309. result.SetColumn(1, new Vector4(1f, 0f, 0f, 0f));
  310. result.SetColumn(2, new Vector4(0f, 0f, 1f, 0f));
  311. result.SetColumn(3, new Vector4(0f, 0f, 0f, 1f));
  312. break;
  313. default:
  314. throw new System.Exception("Unknown Orientation type");
  315. }
  316. return result;
  317. }
  318. public static Matrix4x4 Matrix4x4FromAffineTransform(float[] affineXfrm)
  319. {
  320. Vector4 v0 = new Vector4(affineXfrm[0], affineXfrm[1], 0, 0);
  321. Vector4 v1 = new Vector4(affineXfrm[2], affineXfrm[3], 0, 0);
  322. Vector4 v2 = new Vector4( 0, 0, 1, 0);
  323. Vector4 v3 = new Vector4(affineXfrm[4], affineXfrm[5], 0, 1);
  324. return new Matrix4x4(v0, v1, v2, v3);
  325. }
  326. public static int ConvertTimeSecondsToFrame(double seconds, float frameRate)
  327. {
  328. // NOTE: Generally you should use RountToInt when converting from time to frame number
  329. // but because we're adding a half frame offset (which seems to be the safer thing to do) we need to FloorToInt
  330. seconds = System.Math.Max(0.0, seconds);
  331. frameRate = Mathf.Max(0f, frameRate);
  332. return (int)System.Math.Floor(frameRate * seconds);
  333. }
  334. public static double ConvertFrameToTimeSeconds(int frame, float frameRate)
  335. {
  336. frame = Mathf.Max(0, frame);
  337. frameRate = Mathf.Max(0f, frameRate);
  338. double frameDurationSeconds = 1.0 / frameRate;
  339. return ((double)frame * frameDurationSeconds) + (frameDurationSeconds * 0.5); // Add half a frame we that the time lands in the middle of the frame range and not at the edges
  340. }
  341. public static double FindNextKeyFrameTimeSeconds(double seconds, float frameRate, int keyFrameInterval)
  342. {
  343. seconds = System.Math.Max(0.0, seconds);
  344. frameRate = Mathf.Max(0f, frameRate);
  345. keyFrameInterval = Mathf.Max(0, keyFrameInterval);
  346. int currentFrame = Helper.ConvertTimeSecondsToFrame(seconds, frameRate);
  347. // TODO: allow specifying a minimum number of frames so that if currentFrame is too close to nextKeyFrame, it will calculate the next-next keyframe
  348. int nextKeyFrame = keyFrameInterval * Mathf.CeilToInt((float)(currentFrame + 1) / (float)keyFrameInterval);
  349. return Helper.ConvertFrameToTimeSeconds(nextKeyFrame, frameRate);
  350. }
  351. public static System.DateTime ConvertSecondsSince1970ToDateTime(double secondsSince1970)
  352. {
  353. System.TimeSpan time = System.TimeSpan.FromSeconds(secondsSince1970);
  354. return new System.DateTime(1970, 1, 1).Add(time);
  355. }
  356. #if (UNITY_EDITOR_WIN || (!UNITY_EDITOR && UNITY_STANDALONE_WIN))
  357. [System.Runtime.InteropServices.DllImport("kernel32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode, EntryPoint = "GetShortPathNameW", SetLastError=true)]
  358. private static extern int GetShortPathName([System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPWStr)] string pathName,
  359. [System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPWStr)] System.Text.StringBuilder shortName,
  360. int cbShortName);
  361. // Handle very long file paths by converting to DOS 8.3 format
  362. internal static string ConvertLongPathToShortDOS83Path(string path)
  363. {
  364. const string pathToken = @"\\?\";
  365. string result = pathToken + path.Replace("/","\\");
  366. int length = GetShortPathName(result, null, 0);
  367. if (length > 0)
  368. {
  369. System.Text.StringBuilder sb = new System.Text.StringBuilder(length);
  370. if (0 != GetShortPathName(result, sb, length))
  371. {
  372. result = sb.ToString().Replace(pathToken, "");
  373. Debug.LogWarning("[AVProVideo] Long path detected. Changing to DOS 8.3 format");
  374. }
  375. }
  376. return result;
  377. }
  378. #endif
  379. // Converts a non-readable texture to a readable Texture2D.
  380. // "targetTexture" can be null or you can pass in an existing texture.
  381. // Remember to Destroy() the returned texture after finished with it
  382. public static Texture2D GetReadableTexture(Texture inputTexture, bool requiresVerticalFlip, Orientation ori, Texture2D targetTexture = null)
  383. {
  384. Texture2D resultTexture = targetTexture;
  385. RenderTexture prevRT = RenderTexture.active;
  386. int textureWidth = inputTexture.width;
  387. int textureHeight = inputTexture.height;
  388. #if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE || UNITY_IOS || UNITY_TVOS
  389. if (ori == Orientation.Portrait || ori == Orientation.PortraitFlipped)
  390. {
  391. textureWidth = inputTexture.height;
  392. textureHeight = inputTexture.width;
  393. }
  394. #endif
  395. // Blit the texture to a temporary RenderTexture
  396. // This handles any format conversion that is required and allows us to use ReadPixels to copy texture from RT to readable texture
  397. RenderTexture tempRT = RenderTexture.GetTemporary(textureWidth, textureHeight, 0, RenderTextureFormat.ARGB32);
  398. if (ori == Orientation.Landscape)
  399. {
  400. if (!requiresVerticalFlip)
  401. {
  402. Graphics.Blit(inputTexture, tempRT);
  403. }
  404. else
  405. {
  406. // The above Blit can't flip unless using a material, so we use Graphics.DrawTexture instead
  407. GL.PushMatrix();
  408. RenderTexture.active = tempRT;
  409. GL.LoadPixelMatrix(0f, tempRT.width, 0f, tempRT.height);
  410. Rect sourceRect = new Rect(0f, 0f, 1f, 1f);
  411. // NOTE: not sure why we need to set y to -1, without this there is a 1px gap at the bottom
  412. Rect destRect = new Rect(0f, -1f, tempRT.width, tempRT.height);
  413. Graphics.DrawTexture(destRect, inputTexture, sourceRect, 0, 0, 0, 0);
  414. GL.PopMatrix();
  415. GL.InvalidateState();
  416. }
  417. }
  418. #if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE || UNITY_IOS || UNITY_TVOS
  419. else
  420. {
  421. Matrix4x4 m = Matrix4x4.identity;
  422. switch (ori)
  423. {
  424. case Orientation.Portrait:
  425. m = Matrix4x4.TRS(new Vector3(0f, inputTexture.width, 0f), Quaternion.Euler(0f, 0f, -90f), Vector3.one);
  426. break;
  427. case Orientation.PortraitFlipped:
  428. m = Matrix4x4.TRS(new Vector3(inputTexture.height, 0f, 0f), Quaternion.Euler(0f, 0f, 90f), Vector3.one);
  429. break;
  430. case Orientation.LandscapeFlipped:
  431. m = Matrix4x4.TRS(new Vector3(inputTexture.width, inputTexture.height, 0f), Quaternion.identity, new Vector3(-1f, -1f, 1f));
  432. break;
  433. }
  434. // The above Blit can't flip unless using a material, so we use Graphics.DrawTexture instead
  435. GL.InvalidateState();
  436. RenderTexture.active = tempRT;
  437. GL.Clear(false, true, Color.black);
  438. GL.PushMatrix();
  439. GL.LoadPixelMatrix(0f, tempRT.width, 0f, tempRT.height);
  440. Rect sourceRect = new Rect(0f, 0f, 1f, 1f);
  441. // NOTE: not sure why we need to set y to -1, without this there is a 1px gap at the bottom
  442. Rect destRect = new Rect(0f, -1f, inputTexture.width, inputTexture.height);
  443. GL.MultMatrix(m);
  444. Graphics.DrawTexture(destRect, inputTexture, sourceRect, 0, 0, 0, 0);
  445. GL.PopMatrix();
  446. GL.InvalidateState();
  447. }
  448. #endif
  449. if (resultTexture == null)
  450. {
  451. resultTexture = new Texture2D(textureWidth, textureHeight, TextureFormat.ARGB32, false);
  452. }
  453. RenderTexture.active = tempRT;
  454. resultTexture.ReadPixels(new Rect(0f, 0f, textureWidth, textureHeight), 0, 0, false);
  455. resultTexture.Apply(false, false);
  456. RenderTexture.ReleaseTemporary(tempRT);
  457. RenderTexture.active = prevRT;
  458. return resultTexture;
  459. }
  460. // Converts a non-readable texture to a readable Texture2D.
  461. // "targetTexture" can be null or you can pass in an existing texture.
  462. // Remember to Destroy() the returned texture after finished with it
  463. public static Texture2D GetReadableTexture(RenderTexture inputTexture, Texture2D targetTexture = null)
  464. {
  465. if (targetTexture == null)
  466. {
  467. targetTexture = new Texture2D(inputTexture.width, inputTexture.height, TextureFormat.ARGB32, false);
  468. }
  469. RenderTexture prevRT = RenderTexture.active;
  470. RenderTexture.active = inputTexture;
  471. targetTexture.ReadPixels(new Rect(0f, 0f, inputTexture.width, inputTexture.height), 0, 0, false);
  472. targetTexture.Apply(false, false);
  473. RenderTexture.active = prevRT;
  474. return targetTexture;
  475. }
  476. }
  477. }