Browse Source

First Commit

DGJ 1 year ago
commit
25eddea8c0
100 changed files with 9383 additions and 0 deletions
  1. 8 0
      package/Assets.meta
  2. 8 0
      package/Contents.meta
  3. 8 0
      package/Editor.meta
  4. 9 0
      package/Editor/Resources.meta
  5. BIN
      package/Editor/Resources/AVProVideoIcon.png
  6. 90 0
      package/Editor/Resources/AVProVideoIcon.png.meta
  7. 9 0
      package/Editor/Scripts.meta
  8. 194 0
      package/Editor/Scripts/AnimCollapseSection.cs
  9. 12 0
      package/Editor/Scripts/AnimCollapseSection.cs.meta
  10. 9 0
      package/Editor/Scripts/Components.meta
  11. 108 0
      package/Editor/Scripts/Components/ApplyToMaterialEditor.cs
  12. 12 0
      package/Editor/Scripts/Components/ApplyToMaterialEditor.cs.meta
  13. 183 0
      package/Editor/Scripts/Components/ApplyToMeshEditor.cs
  14. 8 0
      package/Editor/Scripts/Components/ApplyToMeshEditor.cs.meta
  15. 96 0
      package/Editor/Scripts/Components/AudioOutputEditor.cs
  16. 12 0
      package/Editor/Scripts/Components/AudioOutputEditor.cs.meta
  17. 86 0
      package/Editor/Scripts/Components/DisplayIMGUIEditor.cs
  18. 8 0
      package/Editor/Scripts/Components/DisplayIMGUIEditor.cs.meta
  19. 701 0
      package/Editor/Scripts/Components/MediaPlayerEditor.cs
  20. 12 0
      package/Editor/Scripts/Components/MediaPlayerEditor.cs.meta
  21. 160 0
      package/Editor/Scripts/Components/MediaPlayerEditor_AboutHelp.cs
  22. 12 0
      package/Editor/Scripts/Components/MediaPlayerEditor_AboutHelp.cs.meta
  23. 185 0
      package/Editor/Scripts/Components/MediaPlayerEditor_Android.cs
  24. 12 0
      package/Editor/Scripts/Components/MediaPlayerEditor_Android.cs.meta
  25. 140 0
      package/Editor/Scripts/Components/MediaPlayerEditor_Apple.cs
  26. 12 0
      package/Editor/Scripts/Components/MediaPlayerEditor_Apple.cs.meta
  27. 87 0
      package/Editor/Scripts/Components/MediaPlayerEditor_Audio.cs
  28. 12 0
      package/Editor/Scripts/Components/MediaPlayerEditor_Audio.cs.meta
  29. 284 0
      package/Editor/Scripts/Components/MediaPlayerEditor_Debug.cs
  30. 12 0
      package/Editor/Scripts/Components/MediaPlayerEditor_Debug.cs.meta
  31. 40 0
      package/Editor/Scripts/Components/MediaPlayerEditor_Events.cs
  32. 12 0
      package/Editor/Scripts/Components/MediaPlayerEditor_Events.cs.meta
  33. 78 0
      package/Editor/Scripts/Components/MediaPlayerEditor_Global.cs
  34. 12 0
      package/Editor/Scripts/Components/MediaPlayerEditor_Global.cs.meta
  35. 130 0
      package/Editor/Scripts/Components/MediaPlayerEditor_Network.cs
  36. 12 0
      package/Editor/Scripts/Components/MediaPlayerEditor_Network.cs.meta
  37. 368 0
      package/Editor/Scripts/Components/MediaPlayerEditor_Platforms.cs
  38. 12 0
      package/Editor/Scripts/Components/MediaPlayerEditor_Platforms.cs.meta
  39. 874 0
      package/Editor/Scripts/Components/MediaPlayerEditor_Player.cs
  40. 12 0
      package/Editor/Scripts/Components/MediaPlayerEditor_Player.cs.meta
  41. 389 0
      package/Editor/Scripts/Components/MediaPlayerEditor_Source.cs
  42. 12 0
      package/Editor/Scripts/Components/MediaPlayerEditor_Source.cs.meta
  43. 77 0
      package/Editor/Scripts/Components/MediaPlayerEditor_Subtitles.cs
  44. 12 0
      package/Editor/Scripts/Components/MediaPlayerEditor_Subtitles.cs.meta
  45. 36 0
      package/Editor/Scripts/Components/MediaPlayerEditor_WebGL.cs
  46. 12 0
      package/Editor/Scripts/Components/MediaPlayerEditor_WebGL.cs.meta
  47. 319 0
      package/Editor/Scripts/Components/MediaPlayerEditor_Windows.cs
  48. 12 0
      package/Editor/Scripts/Components/MediaPlayerEditor_Windows.cs.meta
  49. 517 0
      package/Editor/Scripts/Components/PlaylistMediaPlayerEditor.cs
  50. 8 0
      package/Editor/Scripts/Components/PlaylistMediaPlayerEditor.cs.meta
  51. 118 0
      package/Editor/Scripts/Components/ResolveToRenderTextureEditor.cs
  52. 12 0
      package/Editor/Scripts/Components/ResolveToRenderTextureEditor.cs.meta
  53. 407 0
      package/Editor/Scripts/EditorHelper.cs
  54. 12 0
      package/Editor/Scripts/EditorHelper.cs.meta
  55. 44 0
      package/Editor/Scripts/MediaHintsDrawer.cs
  56. 12 0
      package/Editor/Scripts/MediaHintsDrawer.cs.meta
  57. 62 0
      package/Editor/Scripts/MediaPathDrawer.cs
  58. 12 0
      package/Editor/Scripts/MediaPathDrawer.cs.meta
  59. 388 0
      package/Editor/Scripts/MediaReferenceEditor.cs
  60. 11 0
      package/Editor/Scripts/MediaReferenceEditor.cs.meta
  61. 313 0
      package/Editor/Scripts/PluginProcessor.cs
  62. 12 0
      package/Editor/Scripts/PluginProcessor.cs.meta
  63. 33 0
      package/Editor/Scripts/PostProcessBuild.cs
  64. 8 0
      package/Editor/Scripts/PostProcessBuild.cs.meta
  65. 65 0
      package/Editor/Scripts/PostProcessBuild_Android.cs
  66. 11 0
      package/Editor/Scripts/PostProcessBuild_Android.cs.meta
  67. 270 0
      package/Editor/Scripts/PostProcessBuild_iOS.cs
  68. 12 0
      package/Editor/Scripts/PostProcessBuild_iOS.cs.meta
  69. 170 0
      package/Editor/Scripts/PostProcessBuild_macOS.cs
  70. 11 0
      package/Editor/Scripts/PostProcessBuild_macOS.cs.meta
  71. 121 0
      package/Editor/Scripts/PreProcessBuild.cs
  72. 12 0
      package/Editor/Scripts/PreProcessBuild.cs.meta
  73. 91 0
      package/Editor/Scripts/RecentItems.cs
  74. 12 0
      package/Editor/Scripts/RecentItems.cs.meta
  75. 289 0
      package/Editor/Scripts/RecentMenu.cs
  76. 12 0
      package/Editor/Scripts/RecentMenu.cs.meta
  77. 379 0
      package/Editor/Scripts/SupportWindow.cs
  78. 8 0
      package/Editor/Scripts/SupportWindow.cs.meta
  79. 71 0
      package/Editor/Scripts/VideoResolveOptionsDrawer.cs
  80. 12 0
      package/Editor/Scripts/VideoResolveOptionsDrawer.cs.meta
  81. 7 0
      package/Editor/_AVProVideo.Editor.asmdef
  82. 8 0
      package/Editor/_AVProVideo.Editor.asmdef.meta
  83. 8 0
      package/Platform.meta
  84. 8 0
      package/Resources.meta
  85. 8 0
      package/Resources/Resources.meta
  86. 9 0
      package/Resources/Resources/Textures.meta
  87. BIN
      package/Resources/Resources/Textures/AVProVideo-NullPlayer-Frame0.png
  88. 90 0
      package/Resources/Resources/Textures/AVProVideo-NullPlayer-Frame0.png.meta
  89. BIN
      package/Resources/Resources/Textures/AVProVideo-NullPlayer-Frame1.png
  90. 90 0
      package/Resources/Resources/Textures/AVProVideo-NullPlayer-Frame1.png.meta
  91. 8 0
      package/Resources/Scripts.meta
  92. 9 0
      package/Resources/Scripts/AssetTypes.meta
  93. 118 0
      package/Resources/Scripts/AssetTypes/MediaReference.cs
  94. 12 0
      package/Resources/Scripts/AssetTypes/MediaReference.cs.meta
  95. 9 0
      package/Resources/Scripts/Components.meta
  96. 231 0
      package/Resources/Scripts/Components/ApplyToMaterial.cs
  97. 8 0
      package/Resources/Scripts/Components/ApplyToMaterial.cs.meta
  98. 248 0
      package/Resources/Scripts/Components/ApplyToMesh.cs
  99. 8 0
      package/Resources/Scripts/Components/ApplyToMesh.cs.meta
  100. 81 0
      package/Resources/Scripts/Components/AudioChannelMixer.cs

+ 8 - 0
package/Assets.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 0c8bb1dc1e5cfd142919e67bc72e2644
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
package/Contents.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 955268ba53df7834f89c0c6fe9d07c55
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
package/Editor.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: a843783554172814e8b5d382776b4c83
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 0
package/Editor/Resources.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 44c1fb33835a7a040a58adb2e708afb4
+folderAsset: yes
+timeCreated: 1551713189
+licenseType: Store
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

BIN
package/Editor/Resources/AVProVideoIcon.png


+ 90 - 0
package/Editor/Resources/AVProVideoIcon.png.meta

@@ -0,0 +1,90 @@
+fileFormatVersion: 2
+guid: bb83b41b53a59874692b83eab5873998
+TextureImporter:
+  fileIDToRecycleName: {}
+  serializedVersion: 4
+  mipmaps:
+    mipMapMode: 0
+    enableMipMap: 1
+    sRGBTexture: 1
+    linearTexture: 1
+    fadeOut: 0
+    borderMipMap: 0
+    mipMapFadeDistanceStart: 1
+    mipMapFadeDistanceEnd: 3
+  bumpmap:
+    convertToNormalMap: 0
+    externalNormalMap: 0
+    heightScale: 0.25
+    normalMapFilter: 0
+  isReadable: 0
+  grayScaleToAlpha: 0
+  generateCubemap: 6
+  cubemapConvolution: 0
+  seamlessCubemap: 0
+  textureFormat: -3
+  maxTextureSize: 1024
+  textureSettings:
+    filterMode: -1
+    aniso: 1
+    mipBias: -1
+    wrapMode: 0
+  nPOTScale: 1
+  lightmap: 0
+  compressionQuality: 50
+  spriteMode: 0
+  spriteExtrude: 1
+  spriteMeshType: 1
+  alignment: 0
+  spritePivot: {x: 0.5, y: 0.5}
+  spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+  spritePixelsToUnits: 100
+  alphaUsage: 0
+  alphaIsTransparency: 0
+  spriteTessellationDetail: -1
+  textureType: 0
+  textureShape: 1
+  maxTextureSizeSet: 0
+  compressionQualitySet: 0
+  textureFormatSet: 0
+  platformSettings:
+  - buildTarget: DefaultTexturePlatform
+    maxTextureSize: 1024
+    textureFormat: -1
+    textureCompression: 0
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+  - buildTarget: Standalone
+    maxTextureSize: 1024
+    textureFormat: -1
+    textureCompression: 0
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+  - buildTarget: iPhone
+    maxTextureSize: 1024
+    textureFormat: -1
+    textureCompression: 0
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+  - buildTarget: Android
+    maxTextureSize: 1024
+    textureFormat: -1
+    textureCompression: 0
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+  spriteSheet:
+    serializedVersion: 2
+    sprites: []
+    outline: []
+  spritePackingTag: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 0
package/Editor/Scripts.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: fa40b7e33f12f754586d06e6df3d98ba
+folderAsset: yes
+timeCreated: 1551712689
+licenseType: Store
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 194 - 0
package/Editor/Scripts/AnimCollapseSection.cs

@@ -0,0 +1,194 @@
+#define AVPROVIDEO_SUPPORT_LIVEEDITMODE
+using UnityEngine;
+using UnityEditor;
+using System.Collections.Generic;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// A collapsable GUI section that animates during open and close
+	internal class AnimCollapseSection
+	{
+		internal const string SettingsPrefix = "AVProVideo-MediaPlayerEditor-";
+		private const float CollapseSpeed = 2f;
+		private static GUIStyle _styleCollapsableSection = null;
+		private static GUIStyle _styleButtonFoldout = null;
+		private static GUIStyle _styleHelpBoxNoPad = null;
+
+		public AnimCollapseSection(string label, bool showOnlyInEditMode, bool isDefaultExpanded, System.Action action, UnityEditor.Editor editor, Color backgroundColor, List<AnimCollapseSection> groupItems = null)
+			: this(new GUIContent(label), showOnlyInEditMode, isDefaultExpanded, action, editor, backgroundColor, groupItems)
+		{
+		}
+		public AnimCollapseSection(GUIContent label, bool showOnlyInEditMode, bool isDefaultExpanded, System.Action action, UnityEditor.Editor editor, Color backgroundColor, List<AnimCollapseSection> groupItems = null)
+		{
+			Label = label;
+			_name = Label.text;
+			Label.text = " " + Label.text;		// Add a space for aesthetics
+			ShowOnlyInEditMode = showOnlyInEditMode;
+			_action = action;
+			isDefaultExpanded = EditorPrefs.GetBool(PrefName, isDefaultExpanded);
+			BackgroundColor = backgroundColor;
+			_groupItems = groupItems;
+			_anim = new UnityEditor.AnimatedValues.AnimBool(isDefaultExpanded);
+			_anim.speed = CollapseSpeed;
+			_anim.valueChanged.AddListener(editor.Repaint);
+		}
+		~AnimCollapseSection()
+		{
+			_anim.valueChanged.RemoveAllListeners();
+		}
+
+		private string _name;
+		private UnityEditor.AnimatedValues.AnimBool _anim;
+		private System.Action _action;
+		private List<AnimCollapseSection> _groupItems;
+
+		public void Invoke()
+		{
+			_action.Invoke();
+		}
+
+		public bool IsExpanded { get { return _anim.target; } set { if (_anim.target != value) { _anim.target = value; if (value) CollapseSiblings(); } } }
+		public float Faded { get { return _anim.faded; } }
+		public GUIContent Label { get; private set; }
+		public bool ShowOnlyInEditMode { get; private set; }
+		public Color BackgroundColor { get; private set; }
+		private string PrefName { get { return GetPrefName(_name); } }
+
+		public void Save()
+		{
+			EditorPrefs.SetBool(PrefName, IsExpanded);
+		}
+
+		private void CollapseSiblings()
+		{
+			// Ensure only a single item is in an expanded state
+			if (_groupItems != null)
+			{
+				foreach (AnimCollapseSection section in _groupItems)
+				{
+					if (section != this && section.IsExpanded)
+					{
+						section.IsExpanded = false;
+					}
+				}
+			}
+		}
+
+		internal static string GetPrefName(string label)
+		{
+			return SettingsPrefix + "Expand-" + label;
+		}
+
+		internal static void CreateStyles()
+		{
+			if (_styleCollapsableSection == null)
+			{
+				_styleCollapsableSection = new GUIStyle(GUI.skin.box);
+				_styleCollapsableSection.padding.top = 0;
+				_styleCollapsableSection.padding.bottom = 0;
+			}
+			if (_styleButtonFoldout == null)
+			{
+				_styleButtonFoldout = new GUIStyle(EditorStyles.foldout);
+				_styleButtonFoldout.margin = new RectOffset();
+				_styleButtonFoldout.fontStyle = FontStyle.Bold;
+				_styleButtonFoldout.alignment = TextAnchor.MiddleLeft;
+			}
+			if (_styleHelpBoxNoPad == null)
+			{
+				_styleHelpBoxNoPad = new GUIStyle(EditorStyles.helpBox);
+				_styleHelpBoxNoPad.padding = new RectOffset();
+				//_styleHelpBoxNoPad.border = new RectOffset();
+				_styleHelpBoxNoPad.overflow = new RectOffset();
+				_styleHelpBoxNoPad.margin = new RectOffset();
+				_styleHelpBoxNoPad.margin = new RectOffset(8, 0, 0, 0);
+				_styleHelpBoxNoPad.stretchWidth = false;
+				_styleHelpBoxNoPad.stretchHeight = false;
+				//_styleHelpBoxNoPad.normal.background = Texture2D.whiteTexture;
+			}
+		}
+
+		internal static void Show(AnimCollapseSection section, int indentLevel = 0)
+		{
+			if (section.ShowOnlyInEditMode && Application.isPlaying) return;
+
+			float headerGlow = Mathf.Lerp(0.5f, 0.85f, section.Faded);
+			//float headerGlow = Mathf.Lerp(0.85f, 1f, section.Faded);
+			if (EditorGUIUtility.isProSkin)
+			{
+				GUI.backgroundColor = section.BackgroundColor * new Color(headerGlow, headerGlow, headerGlow, 1f);
+			}
+			else
+			{
+				headerGlow = Mathf.Lerp(0.75f, 1f, section.Faded);
+				GUI.backgroundColor = section.BackgroundColor * new Color(headerGlow, headerGlow, headerGlow, 1f);
+			}
+			GUILayout.BeginVertical(_styleHelpBoxNoPad);
+			GUILayout.Box(GUIContent.none, EditorStyles.miniButton, GUILayout.ExpandWidth(true));
+			GUI.backgroundColor = Color.white;
+			Rect buttonRect = GUILayoutUtility.GetLastRect();
+			if (Event.current.type != EventType.Layout)
+			{
+				buttonRect.xMin += indentLevel * EditorGUIUtility.fieldWidth / 3f;
+				EditorGUI.indentLevel++;
+				EditorGUIUtility.SetIconSize(new Vector2(16f, 16f));
+				section.IsExpanded = EditorGUI.Foldout(buttonRect, section.IsExpanded, section.Label, true, _styleButtonFoldout);
+				EditorGUIUtility.SetIconSize(Vector2.zero);
+				EditorGUI.indentLevel--;
+			}
+
+			if (EditorGUILayout.BeginFadeGroup(section.Faded))
+			{
+				section.Invoke();
+			}
+			EditorGUILayout.EndFadeGroup();
+			GUILayout.EndVertical();
+		}
+
+		internal static void Show(string label, ref bool isExpanded, System.Action action, bool showOnlyInEditMode)
+		{
+			if (showOnlyInEditMode && Application.isPlaying) return;
+
+			if (BeginShow(label, ref isExpanded, Color.white))
+			{
+				action.Invoke();
+			}
+			EndShow();
+		}
+
+		internal static bool BeginShow(string label, ref bool isExpanded, Color tintColor)
+		{
+			GUI.color = Color.white;
+			GUI.backgroundColor = Color.clear;
+			if (isExpanded)
+			{
+				GUI.color = Color.white;
+				GUI.backgroundColor = new Color(0.8f, 0.8f, 0.8f, 0.1f);
+				if (EditorGUIUtility.isProSkin)
+				{
+					GUI.backgroundColor = Color.black;
+				}
+			}
+
+			GUILayout.BeginVertical(_styleCollapsableSection);
+			GUI.color = tintColor;
+			GUI.backgroundColor = Color.white;
+			if (GUILayout.Button(label, EditorStyles.toolbarButton))
+			{
+				isExpanded = !isExpanded;
+			}
+			GUI.color = Color.white;
+
+			return isExpanded;
+		}
+
+		internal static void EndShow()
+		{
+			GUILayout.EndVertical();
+		}
+	}
+}

+ 12 - 0
package/Editor/Scripts/AnimCollapseSection.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 7ca21d2c4a039f347a66568bb23b62bc
+timeCreated: 1448902492
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 0
package/Editor/Scripts/Components.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: df15cc9892e0644469f075f3a1c0c8f4
+folderAsset: yes
+timeCreated: 1591790246
+licenseType: Store
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 108 - 0
package/Editor/Scripts/Components/ApplyToMaterialEditor.cs

@@ -0,0 +1,108 @@
+using UnityEngine;
+using UnityEditor;
+using System.Collections.Generic;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// Editor for the ApplyToMaterial component
+	/// </summary>
+	[CanEditMultipleObjects]
+	[CustomEditor(typeof(ApplyToMaterial))]
+	public class ApplyToMaterialEditor : UnityEditor.Editor
+	{
+		private static readonly GUIContent _guiTextTextureProperty = new GUIContent("Texture Property");
+
+		private SerializedProperty _propTextureOffset;
+		private SerializedProperty _propTextureScale;
+		private SerializedProperty _propMediaPlayer;
+		private SerializedProperty _propMaterial;
+		private SerializedProperty _propTexturePropertyName;
+		private SerializedProperty _propDefaultTexture;
+		private GUIContent[] _materialTextureProperties = new GUIContent[0];
+
+		void OnEnable()
+		{
+			_propTextureOffset = this.CheckFindProperty("_offset");
+			_propTextureScale = this.CheckFindProperty("_scale");
+			_propMediaPlayer = this.CheckFindProperty("_media");
+			_propMaterial = this.CheckFindProperty("_material");
+			_propTexturePropertyName = this.CheckFindProperty("_texturePropertyName");
+			_propDefaultTexture = this.CheckFindProperty("_defaultTexture");
+		}
+
+		public override void OnInspectorGUI()
+		{
+			serializedObject.Update();
+
+			if (_propMaterial == null)
+			{
+				return;
+			}
+
+			EditorGUI.BeginChangeCheck();
+
+			EditorGUILayout.PropertyField(_propMediaPlayer);
+			EditorGUILayout.PropertyField(_propDefaultTexture);
+			EditorGUILayout.PropertyField(_propMaterial);
+
+			bool hasKeywords = false;
+			int texturePropertyIndex = 0;
+			if (_propMaterial.objectReferenceValue != null)
+			{
+				Material mat = (Material)(_propMaterial.objectReferenceValue);
+
+				if (mat.shaderKeywords.Length > 0)
+				{
+					hasKeywords = true;
+				}
+
+				MaterialProperty[] matProps = MaterialEditor.GetMaterialProperties(new UnityEngine.Object[] { mat });
+
+				List<GUIContent> items = new List<GUIContent>(16);
+				foreach (MaterialProperty matProp in matProps)
+				{
+					if (matProp.type == MaterialProperty.PropType.Texture)
+					{
+						if (matProp.name == _propTexturePropertyName.stringValue)
+						{
+							texturePropertyIndex = items.Count;
+						}
+						items.Add(new GUIContent(matProp.name));
+					}
+				}
+				_materialTextureProperties = items.ToArray();
+			}
+
+			int newTexturePropertyIndex = EditorGUILayout.Popup(_guiTextTextureProperty, texturePropertyIndex, _materialTextureProperties);
+			if (newTexturePropertyIndex >= 0 && newTexturePropertyIndex < _materialTextureProperties.Length)
+			{
+				_propTexturePropertyName.stringValue = _materialTextureProperties[newTexturePropertyIndex].text;
+			}
+
+			if (hasKeywords && _propTexturePropertyName.stringValue != Helper.UnityBaseTextureName)
+			{
+				EditorGUILayout.HelpBox("When using an uber shader you may need to enable the keywords on a material for certain texture slots to take effect.  You can sometimes achieve this (eg with Standard shader) by putting a dummy texture into the texture slot.", MessageType.Info);
+			}
+
+			EditorGUILayout.PropertyField(_propTextureOffset);
+			EditorGUILayout.PropertyField(_propTextureScale);
+
+			serializedObject.ApplyModifiedProperties();
+
+			bool wasModified = EditorGUI.EndChangeCheck();
+
+			if (Application.isPlaying && wasModified)
+			{
+				foreach (Object obj in this.targets)
+				{
+					((ApplyToMaterial)obj).ForceUpdate();
+				}
+			}
+		}
+	}
+}

+ 12 - 0
package/Editor/Scripts/Components/ApplyToMaterialEditor.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 60268ddd706f2f1469acb32edab1dea9
+timeCreated: 1591790256
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 183 - 0
package/Editor/Scripts/Components/ApplyToMeshEditor.cs

@@ -0,0 +1,183 @@
+using UnityEngine;
+using UnityEditor;
+using System.Collections.Generic;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// Editor for the ApplyToMesh component
+	/// </summary>
+	[CanEditMultipleObjects]
+	[CustomEditor(typeof(ApplyToMesh))]
+	public class ApplyToMeshEditor : UnityEditor.Editor
+	{
+		private static readonly GUIContent _guiTextTextureProperty = new GUIContent("Texture Property");
+
+		private SerializedProperty _propTextureOffset;
+		private SerializedProperty _propTextureScale;
+		private SerializedProperty _propMediaPlayer;
+		private SerializedProperty _propRenderer;
+		private SerializedProperty _propMaterialIndex;
+		private SerializedProperty _propTexturePropertyName;
+		private SerializedProperty _propDefaultTexture;
+		private SerializedProperty _propAutomaticStereoPacking;
+		private SerializedProperty _propOverrideStereoPacking;
+		private SerializedProperty _propStereoRedGreenTint;
+		private GUIContent[] _materialTextureProperties = new GUIContent[0];
+
+		void OnEnable()
+		{
+			_propTextureOffset = this.CheckFindProperty("_offset");
+			_propTextureScale = this.CheckFindProperty("_scale");
+			_propMediaPlayer = this.CheckFindProperty("_media");
+			_propRenderer = this.CheckFindProperty("_renderer");
+			_propMaterialIndex = this.CheckFindProperty("_materialIndex");
+			_propTexturePropertyName = this.CheckFindProperty("_texturePropertyName");
+			_propDefaultTexture = this.CheckFindProperty("_defaultTexture");
+			_propAutomaticStereoPacking = this.CheckFindProperty("_automaticStereoPacking");
+			_propOverrideStereoPacking = this.CheckFindProperty("_overrideStereoPacking");
+			_propStereoRedGreenTint = this.CheckFindProperty("_stereoRedGreenTint");
+		}
+
+		public override void OnInspectorGUI()
+		{
+			serializedObject.Update();
+
+			if (_propRenderer == null)
+			{
+				return;
+			}
+
+			EditorGUI.BeginChangeCheck();
+
+			EditorGUILayout.PropertyField(_propMediaPlayer);
+			EditorGUILayout.PropertyField(_propDefaultTexture);
+			EditorGUILayout.PropertyField(_propRenderer);
+
+			bool hasKeywords = false;
+			int materialCount = 0;
+			int texturePropertyIndex = 0;
+			_materialTextureProperties = new GUIContent[0];
+			if (_propRenderer.objectReferenceValue != null)
+			{
+				Renderer r = (Renderer)(_propRenderer.objectReferenceValue);
+
+				materialCount = r.sharedMaterials.Length;
+				List<Material> nonNullMaterials = new List<Material>(r.sharedMaterials);
+				// Remove any null materials (otherwise MaterialEditor.GetMaterialProperties() errors)
+				for (int i = 0; i < nonNullMaterials.Count; i++)
+				{
+					if (nonNullMaterials[i] == null)
+					{
+						nonNullMaterials.RemoveAt(i);
+						i--;
+					}
+				}
+				
+				if (nonNullMaterials.Count > 0)
+				{
+					// Detect if there are any keywords
+					foreach (Material mat in nonNullMaterials)
+					{
+						if (mat.shaderKeywords.Length > 0)
+						{
+							hasKeywords = true;
+							break;
+						}
+					}
+
+					// Get unique list of texture property names
+					List<GUIContent> items = new List<GUIContent>(16);
+					List<string> textureNames = new List<string>(8);
+					foreach (Material mat in nonNullMaterials)
+					{
+						// NOTE: we process each material separately instead of passing them all into  MaterialEditor.GetMaterialProperties() as it errors if the materials have different properties
+						MaterialProperty[] matProps = MaterialEditor.GetMaterialProperties(new Object[] { mat });
+						foreach (MaterialProperty matProp in matProps)
+						{
+							if (matProp.type == MaterialProperty.PropType.Texture)
+							{
+								if (!textureNames.Contains(matProp.name))
+								{
+									if (matProp.name == _propTexturePropertyName.stringValue)
+									{
+										texturePropertyIndex = items.Count;
+									}
+									textureNames.Add(matProp.name);
+									items.Add(new GUIContent(matProp.name));
+								}
+							}
+						}
+					}
+					_materialTextureProperties = items.ToArray();
+				}
+			}
+
+			if (materialCount > 0)
+			{
+				GUILayout.BeginHorizontal();
+				EditorGUILayout.PrefixLabel("All Materials");
+				EditorGUI.BeginChangeCheck();
+				EditorGUILayout.Toggle(_propMaterialIndex.intValue < 0);
+				if (EditorGUI.EndChangeCheck())
+				{
+					if (_propMaterialIndex.intValue < 0)
+					{
+						_propMaterialIndex.intValue = 0;
+					}
+					else
+					{
+						_propMaterialIndex.intValue = -1;
+					}
+				}
+				GUILayout.EndHorizontal();
+
+				if (_propMaterialIndex.intValue >= 0)
+				{
+					GUILayout.BeginHorizontal();
+					EditorGUILayout.PrefixLabel("Material Index");
+					_propMaterialIndex.intValue = EditorGUILayout.IntSlider(_propMaterialIndex.intValue, 0, materialCount - 1);
+					GUILayout.EndHorizontal();
+				}
+			}
+
+			int newTexturePropertyIndex = EditorGUILayout.Popup(_guiTextTextureProperty, texturePropertyIndex, _materialTextureProperties);
+			if (newTexturePropertyIndex >= 0 && newTexturePropertyIndex < _materialTextureProperties.Length)
+			{
+				_propTexturePropertyName.stringValue = _materialTextureProperties[newTexturePropertyIndex].text;
+			}
+
+			if (hasKeywords && _propTexturePropertyName.stringValue != Helper.UnityBaseTextureName)
+			{
+				EditorGUILayout.HelpBox("When using an uber shader you may need to enable the keywords on a material for certain texture slots to take effect.  You can sometimes achieve this (eg with Standard shader) by putting a dummy texture into the texture slot.", MessageType.Info);
+			}
+
+			EditorGUILayout.PropertyField(_propTextureOffset);
+			EditorGUILayout.PropertyField(_propTextureScale);
+
+
+			EditorGUILayout.PropertyField(_propAutomaticStereoPacking);
+			if (!_propAutomaticStereoPacking.boolValue)
+			{
+				EditorGUILayout.PropertyField(_propOverrideStereoPacking);
+			}
+			EditorGUILayout.PropertyField(_propStereoRedGreenTint);
+
+			serializedObject.ApplyModifiedProperties();
+
+			bool wasModified = EditorGUI.EndChangeCheck();
+
+			if (Application.isPlaying && wasModified)
+			{
+				foreach (Object obj in this.targets)
+				{
+					((ApplyToMesh)obj).ForceUpdate();
+				}
+			}
+		}
+	}
+}

+ 8 - 0
package/Editor/Scripts/Components/ApplyToMeshEditor.cs.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 114ac842bfcaf0745a5e45cb2a7d6559
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 

+ 96 - 0
package/Editor/Scripts/Components/AudioOutputEditor.cs

@@ -0,0 +1,96 @@
+using UnityEditor;
+using UnityEngine;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// Editor for the AudioOutput component
+	/// </summary>
+	[CanEditMultipleObjects]
+	[CustomEditor(typeof(AudioOutput))]
+	public class AudioOutputEditor : UnityEditor.Editor
+	{
+		private static readonly GUIContent _guiTextChannel = new GUIContent("Channel");
+		private static readonly GUIContent _guiTextChannels = new GUIContent("Channels");
+		private static readonly string[] _channelMaskOptions = { "1", "2", "3", "4", "5", "6", "7", "8" };
+
+		private SerializedProperty _propChannelMask;
+		private SerializedProperty _propAudioOutputMode;
+		private int _unityAudioSampleRate;
+		private int _unityAudioSpeakerCount;
+		private string _bufferedMs;
+
+		void OnEnable()
+		{
+			_propChannelMask = this.CheckFindProperty("_channelMask");
+			_propAudioOutputMode = this.CheckFindProperty("_audioOutputMode");
+			_unityAudioSampleRate = Helper.GetUnityAudioSampleRate();
+			_unityAudioSpeakerCount = Helper.GetUnityAudioSpeakerCount();
+		}
+		
+		public override void OnInspectorGUI()
+		{
+			serializedObject.Update();
+
+			DrawDefaultInspector();
+
+			// Display the channel mask as either a bitfield or value slider
+			if ((AudioOutput.AudioOutputMode)_propAudioOutputMode.enumValueIndex == AudioOutput.AudioOutputMode.MultipleChannels)
+			{
+				_propChannelMask.intValue = EditorGUILayout.MaskField(_guiTextChannels, _propChannelMask.intValue, _channelMaskOptions);
+			}
+			else
+			{
+				int prevVal = 0;
+				for(int i = 0; i < 8; ++i)
+				{
+					if((_propChannelMask.intValue & (1 << i)) > 0)
+					{
+						prevVal = i;
+						break;
+					}
+				}
+				
+				int newVal = Mathf.Clamp(EditorGUILayout.IntSlider(_guiTextChannel, prevVal, 0, 7), 0, 7);
+				_propChannelMask.intValue = 1 << newVal;
+			}
+
+			GUILayout.Label("Unity Audio", EditorStyles.boldLabel);
+			EditorGUILayout.LabelField("Speakers", _unityAudioSpeakerCount.ToString());
+			EditorGUILayout.LabelField("Sample Rate", _unityAudioSampleRate.ToString() + "hz");
+			EditorGUILayout.Space();
+
+			AudioOutput audioOutput = (AudioOutput)this.target;
+			if (audioOutput != null)
+			{
+				if (audioOutput.Player != null && audioOutput.Player.Control != null)
+				{
+					int channelCount = audioOutput.Player.Control.GetAudioChannelCount();
+					if (channelCount >= 0)
+					{
+						GUILayout.Label("Media Audio", EditorStyles.boldLabel);
+						EditorGUILayout.LabelField("Channels: " + channelCount);
+						AudioChannelMaskFlags audioChannels = audioOutput.Player.Control.GetAudioChannelMask();
+						GUILayout.Label(audioChannels.ToString(), EditorHelper.IMGUI.GetWordWrappedTextAreaStyle());
+
+						if (Time.frameCount % 4 == 0)
+						{
+							int bufferedSampleCount = audioOutput.Player.Control.GetAudioBufferedSampleCount();
+							float bufferedMs = (bufferedSampleCount * 1000f) / (_unityAudioSampleRate * channelCount);
+							_bufferedMs = "Buffered: " + bufferedMs.ToString("F2") + "ms";
+						}
+
+						EditorGUILayout.LabelField(_bufferedMs);
+						EditorGUILayout.Space();
+					}
+				}
+			}
+
+			serializedObject.ApplyModifiedProperties();
+		}
+	}
+}

+ 12 - 0
package/Editor/Scripts/Components/AudioOutputEditor.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: f7852924144fc064aad785e5985b5402
+timeCreated: 1495783665
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 86 - 0
package/Editor/Scripts/Components/DisplayIMGUIEditor.cs

@@ -0,0 +1,86 @@
+using UnityEngine;
+using UnityEditor;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// Editor for the DisplayIMGUI component
+	/// </summary>
+	[CanEditMultipleObjects]
+	[CustomEditor(typeof(DisplayIMGUI))]
+	public class DisplayIMGUIEditor : UnityEditor.Editor
+	{
+		private SerializedProperty _propMediaPlayer;
+		private SerializedProperty _propScaleMode;
+		private SerializedProperty _propColor;
+		private SerializedProperty _propAllowTransparency;
+		private SerializedProperty _propUseDepth;
+		private SerializedProperty _propDepth;
+		private SerializedProperty _propAreaFullscreen;
+		private SerializedProperty _propAreaX;
+		private SerializedProperty _propAreaY;
+		private SerializedProperty _propAreaWidth;
+		private SerializedProperty _propAreaHeight;
+		private SerializedProperty _propShowAreaInEditor;
+
+		void OnEnable()
+		{
+			_propMediaPlayer = this.CheckFindProperty("_mediaPlayer");
+			_propScaleMode = this.CheckFindProperty("_scaleMode");
+			_propColor = this.CheckFindProperty("_color");
+			_propAllowTransparency = this.CheckFindProperty("_allowTransparency");
+			_propUseDepth = this.CheckFindProperty("_useDepth");
+			_propDepth = this.CheckFindProperty("_depth");
+			_propAreaFullscreen = this.CheckFindProperty("_isAreaFullScreen");
+			_propAreaX = this.CheckFindProperty("_areaX");
+			_propAreaY = this.CheckFindProperty("_areaY");
+			_propAreaWidth = this.CheckFindProperty("_areaWidth");
+			_propAreaHeight = this.CheckFindProperty("_areaHeight");
+			_propShowAreaInEditor = this.CheckFindProperty("_showAreaInEditor");
+		}
+
+		public override void OnInspectorGUI()
+		{
+			serializedObject.Update();
+
+			EditorGUI.BeginChangeCheck();
+
+			EditorGUILayout.PropertyField(_propMediaPlayer);
+			EditorGUILayout.PropertyField(_propScaleMode);
+			EditorGUILayout.PropertyField(_propColor);
+			EditorGUILayout.PropertyField(_propAllowTransparency);
+			EditorGUILayout.PropertyField(_propUseDepth);
+			if (_propUseDepth.boolValue)
+			{
+				EditorGUILayout.PropertyField(_propDepth);
+			}
+
+			// Area
+			EditorGUILayout.PropertyField(_propAreaFullscreen, new GUIContent("Full Screen"));
+			if (!_propAreaFullscreen.boolValue)
+			{
+				EditorGUILayout.PropertyField(_propAreaX, new GUIContent("X"));
+				EditorGUILayout.PropertyField(_propAreaY, new GUIContent("Y"));
+				EditorGUILayout.PropertyField(_propAreaWidth, new GUIContent("Width"));
+				EditorGUILayout.PropertyField(_propAreaHeight, new GUIContent("Height"));
+			}
+			EditorGUILayout.PropertyField(_propShowAreaInEditor, new GUIContent("Show in Editor"));
+
+			serializedObject.ApplyModifiedProperties();
+
+			// Force update
+			bool unhandledChanges = (EditorGUI.EndChangeCheck() && Application.isPlaying);
+			if (unhandledChanges)
+			{
+				foreach (Object obj in this.targets)
+				{
+					((DisplayIMGUI)obj).Update();
+				}
+			}
+		}
+	}
+}

+ 8 - 0
package/Editor/Scripts/Components/DisplayIMGUIEditor.cs.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 8c822ced482d9444aa15d55b5f9d6e7a
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 

+ 701 - 0
package/Editor/Scripts/Components/MediaPlayerEditor.cs

@@ -0,0 +1,701 @@
+#define AVPROVIDEO_SUPPORT_LIVEEDITMODE
+using UnityEngine;
+using UnityEditor;
+using System.Collections.Generic;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// Editor for the MediaPlayer component
+	/// </summary>
+	[CanEditMultipleObjects]
+	[CustomEditor(typeof(MediaPlayer))]
+	public partial class MediaPlayerEditor : UnityEditor.Editor
+	{
+		internal const string SettingsPrefix = "AVProVideo-MediaPlayerEditor-";
+
+		private SerializedProperty _propAutoOpen;
+		private SerializedProperty _propAutoStart;
+		private SerializedProperty _propLoop;
+		private SerializedProperty _propRate;
+		private SerializedProperty _propPersistent;
+		private SerializedProperty _propFilter;
+		private SerializedProperty _propWrap;
+		private SerializedProperty _propAniso;
+#if AVPRO_FEATURE_VIDEORESOLVE
+		private SerializedProperty _propUseVideoResolve;
+		private SerializedProperty _propVideoResolve;
+		private SerializedProperty _propVideoResolveOptions;
+#endif
+		private SerializedProperty _propResample;
+		private SerializedProperty _propResampleMode;
+		private SerializedProperty _propResampleBufferSize;
+		private SerializedProperty _propVideoMapping;
+		private SerializedProperty _propForceFileFormat;
+		private SerializedProperty _propFallbackMediaHints;
+
+		private static Texture2D _icon;
+		private static bool _isTrialVersion = false;
+		private static GUIStyle _styleSectionBox = null;
+
+		private AnimCollapseSection _sectionMediaInfo;
+		private AnimCollapseSection _sectionDebug;
+		private AnimCollapseSection _sectionSettings;
+		private AnimCollapseSection _sectionAboutHelp;
+		private List<AnimCollapseSection> _settingSections = new List<AnimCollapseSection>(16);
+		private List<AnimCollapseSection> _platformSections = new List<AnimCollapseSection>(8);
+
+		[MenuItem("GameObject/Video/AVPro Video - Media Player", false, 100)]
+		public static void CreateMediaPlayerEditor()
+		{
+			GameObject go = new GameObject("MediaPlayer");
+			go.AddComponent<MediaPlayer>();
+			Selection.activeGameObject = go;
+		}
+
+		[MenuItem("GameObject/Video/AVPro Video - Media Player with Unity Audio", false, 101)]
+		public static void CreateMediaPlayerWithUnityAudioEditor()
+		{
+			GameObject go = new GameObject("MediaPlayer");
+			go.AddComponent<MediaPlayer>();
+			go.AddComponent<AudioSource>();
+			AudioOutput ao = go.AddComponent<AudioOutput>();
+			// Move the AudioOutput component above the AudioSource so that it acts as the audio generator
+			UnityEditorInternal.ComponentUtility.MoveComponentUp(ao);
+			Selection.activeGameObject = go;
+		}
+
+		private static void LoadSettings()
+		{
+			_platformIndex = EditorPrefs.GetInt(SettingsPrefix + "PlatformIndex", -1);
+			_showAlpha = EditorPrefs.GetBool(SettingsPrefix + "ShowAlphaChannel", false);
+			_showPreview = EditorPrefs.GetBool(SettingsPrefix + "ShowPreview", true);
+			_allowDeveloperMode = EditorPrefs.GetBool(SettingsPrefix + "AllowDeveloperMode", false);
+			_HTTPHeadersToggle = EditorPrefs.GetBool(SettingsPrefix + "HTTPHeadersToggle", false);
+			RecentItems.Load();
+		}
+
+		private void SaveSettings()
+		{
+			_sectionMediaInfo.Save();
+			_sectionDebug.Save();
+			_sectionSettings.Save();
+			_sectionAboutHelp.Save();
+
+			foreach (AnimCollapseSection section in _settingSections)
+			{
+				section.Save();
+			}
+			foreach (AnimCollapseSection section in _platformSections)
+			{
+				section.Save();
+			}
+
+			_sectionDevModeState.Save();
+			_sectionDevModeTexture.Save();
+			_sectionDevModePlaybackQuality.Save();
+			_sectionDevModeHapNotchLCDecoder.Save();
+			_sectionDevModeBufferedFrames.Save();
+
+			EditorPrefs.SetInt(SettingsPrefix + "PlatformIndex", _platformIndex);
+			EditorPrefs.SetBool(SettingsPrefix + "ShowAlphaChannel", _showAlpha);
+			EditorPrefs.SetBool(SettingsPrefix + "ShowPreview", _showPreview);
+			EditorPrefs.SetBool(SettingsPrefix + "AllowDeveloperMode", _allowDeveloperMode);
+			EditorPrefs.SetBool(SettingsPrefix + "HTTPHeadersToggle", _HTTPHeadersToggle);
+			RecentItems.Save();
+		}
+
+		//[MenuItem("RenderHeads/AVPro Video/Reset Settings", false, 101)]
+		internal static void DeleteSettings()
+		{
+			EditorPrefs.DeleteKey(SettingsPrefix + "PlatformIndex");
+			EditorPrefs.DeleteKey(SettingsPrefix + "ShowAlphaChannel");
+			EditorPrefs.DeleteKey(SettingsPrefix + "AllowDeveloperMode");
+			EditorPrefs.DeleteKey(SettingsPrefix + "HTTPHeadersToggle");
+
+			EditorPrefs.DeleteKey(AnimCollapseSection.GetPrefName("Media"));
+			EditorPrefs.DeleteKey(AnimCollapseSection.GetPrefName("Debug"));
+			EditorPrefs.DeleteKey(AnimCollapseSection.GetPrefName("Settings"));
+			EditorPrefs.DeleteKey(AnimCollapseSection.GetPrefName("About / Help"));
+			
+			EditorPrefs.DeleteKey(AnimCollapseSection.GetPrefName("Source"));
+			EditorPrefs.DeleteKey(AnimCollapseSection.GetPrefName("Main"));
+			EditorPrefs.DeleteKey(AnimCollapseSection.GetPrefName("Audio"));
+			EditorPrefs.DeleteKey(AnimCollapseSection.GetPrefName("Visual"));
+			EditorPrefs.DeleteKey(AnimCollapseSection.GetPrefName("Network"));
+			EditorPrefs.DeleteKey(AnimCollapseSection.GetPrefName("Media"));
+			EditorPrefs.DeleteKey(AnimCollapseSection.GetPrefName("Subtitles"));
+			EditorPrefs.DeleteKey(AnimCollapseSection.GetPrefName("Events"));
+			EditorPrefs.DeleteKey(AnimCollapseSection.GetPrefName("Platform Specific"));
+			EditorPrefs.DeleteKey(AnimCollapseSection.GetPrefName("Global"));
+
+			EditorPrefs.DeleteKey(AnimCollapseSection.GetPrefName(GetPlatformButtonContent(Platform.Windows).text));
+			EditorPrefs.DeleteKey(AnimCollapseSection.GetPrefName(GetPlatformButtonContent(Platform.MacOSX).text));
+			EditorPrefs.DeleteKey(AnimCollapseSection.GetPrefName(GetPlatformButtonContent(Platform.Android).text));
+			EditorPrefs.DeleteKey(AnimCollapseSection.GetPrefName(GetPlatformButtonContent(Platform.iOS).text));
+			EditorPrefs.DeleteKey(AnimCollapseSection.GetPrefName(GetPlatformButtonContent(Platform.tvOS).text));
+			EditorPrefs.DeleteKey(AnimCollapseSection.GetPrefName(GetPlatformButtonContent(Platform.WindowsUWP).text));
+			EditorPrefs.DeleteKey(AnimCollapseSection.GetPrefName(GetPlatformButtonContent(Platform.WebGL).text));
+		}
+
+		private void CreateSections()
+		{
+			const float colorSaturation = 0.66f;
+			Color mediaInfoColor = Color.HSVToRGB(0.55f, colorSaturation, 1f);
+			Color sourceColor = Color.HSVToRGB(0.4f, colorSaturation, 1f);
+			Color platformSpecificColor = Color.HSVToRGB(0.85f, colorSaturation, 1f);
+			Color platformColor = platformSpecificColor;
+			if (EditorGUIUtility.isProSkin)
+			{ 
+				platformColor *= 0.66f;
+			}
+
+			_sectionMediaInfo = new AnimCollapseSection("Media Info", false, false, OnInspectorGUI_MediaInfo, this, mediaInfoColor);
+			_sectionDebug = new AnimCollapseSection("Debug", false, true, OnInspectorGUI_Debug, this, Color.white);
+			_sectionSettings = new AnimCollapseSection("Settings", false, true, OnInspectorGUI_Settings, this, Color.white);
+			_sectionAboutHelp = new AnimCollapseSection("About / Help", false, false, OnInspectorGUI_About, this, Color.white);
+
+			_settingSections.Clear();
+			_settingSections.Add(new AnimCollapseSection("Source", false, true, OnInspectorGUI_Source, this, sourceColor));
+			_settingSections.Add(new AnimCollapseSection("Main", false, false, OnInspectorGUI_Main, this, Color.white));
+			_settingSections.Add(new AnimCollapseSection("Audio", false, false, OnInspectorGUI_Audio, this, Color.white));
+			_settingSections.Add(new AnimCollapseSection("Visual", true, false, OnInspectorGUI_Visual, this, Color.white));
+			//_settingSections.Add(new AnimCollapseSection("Network", true, false, OnInspectorGUI_Network, this, Color.white));
+			_settingSections.Add(new AnimCollapseSection("Subtitles", true, false, OnInspectorGUI_Subtitles, this, Color.white));
+			_settingSections.Add(new AnimCollapseSection("Events", true, false, OnInspectorGUI_Events, this, Color.white));
+			_settingSections.Add(new AnimCollapseSection("Platform Specific", true, false, OnInspectorGUI_PlatformOverrides, this, platformSpecificColor));
+			_settingSections.Add(new AnimCollapseSection("Global", true, false, OnInspectorGUI_GlobalSettings, this, Color.white));
+
+			_platformSections.Clear();
+			_platformSections.Add(new AnimCollapseSection(GetPlatformButtonContent(Platform.Windows), true, false, OnInspectorGUI_Override_Windows, this, platformColor, _platformSections));
+			_platformSections.Add(new AnimCollapseSection(GetPlatformButtonContent(Platform.MacOSX), true, false, OnInspectorGUI_Override_MacOSX, this, platformColor, _platformSections));
+			_platformSections.Add(new AnimCollapseSection(GetPlatformButtonContent(Platform.Android), true, false, OnInspectorGUI_Override_Android, this, platformColor, _platformSections));
+			_platformSections.Add(new AnimCollapseSection(GetPlatformButtonContent(Platform.iOS), true, false, OnInspectorGUI_Override_iOS, this, platformColor, _platformSections));
+			_platformSections.Add(new AnimCollapseSection(GetPlatformButtonContent(Platform.tvOS), true, false, OnInspectorGUI_Override_tvOS, this, platformColor, _platformSections));
+			_platformSections.Add(new AnimCollapseSection(GetPlatformButtonContent(Platform.WindowsUWP), true, false, OnInspectorGUI_Override_WindowsUWP, this, platformColor, _platformSections));
+			_platformSections.Add(new AnimCollapseSection(GetPlatformButtonContent(Platform.WebGL), true, false, OnInspectorGUI_Override_WebGL, this, platformColor, _platformSections));
+
+			_sectionDevModeState = new AnimCollapseSection("State", false, false, OnInspectorGUI_DevMode_State, this, Color.white);
+			_sectionDevModeTexture = new AnimCollapseSection("Texture", false, false, OnInspectorGUI_DevMode_Texture, this, Color.white);
+			_sectionDevModePlaybackQuality = new AnimCollapseSection("Presentation Quality", false, false, OnInspectorGUI_DevMode_PresentationQuality, this, Color.white);
+			_sectionDevModeHapNotchLCDecoder = new AnimCollapseSection("Hap/NotchLC Decoder", false, false, OnInspectorGUI_DevMode_HapNotchLCDecoder, this, Color.white);
+			_sectionDevModeBufferedFrames = new AnimCollapseSection("Buffers", false, false, OnInspectorGUI_DevMode_BufferedFrames, this, Color.white);
+		}
+
+		private void ResolveProperties()
+		{
+			_propMediaSource = this.CheckFindProperty("_mediaSource");
+			_propMediaReference = this.CheckFindProperty("_mediaReference");
+			_propMediaPath = this.CheckFindProperty("_mediaPath");
+			_propAutoOpen = this.CheckFindProperty("_autoOpen");
+			_propAutoStart = this.CheckFindProperty("_autoPlayOnStart");
+			_propLoop = this.CheckFindProperty("_loop");
+			_propRate = this.CheckFindProperty("_playbackRate");
+			_propVolume = this.CheckFindProperty("_audioVolume");
+			_propBalance = this.CheckFindProperty("_audioBalance");
+			_propMuted = this.CheckFindProperty("_audioMuted");
+			_propPersistent = this.CheckFindProperty("_persistent");
+			_propEvents = this.CheckFindProperty("_events");
+			_propEventMask = this.CheckFindProperty("_eventMask");
+			_propPauseMediaOnAppPause = this.CheckFindProperty("_pauseMediaOnAppPause");
+			_propPlayMediaOnAppUnpause = this.CheckFindProperty("_playMediaOnAppUnpause");
+			_propFilter = this.CheckFindProperty("_textureFilterMode");
+			_propWrap = this.CheckFindProperty("_textureWrapMode");
+			_propAniso = this.CheckFindProperty("_textureAnisoLevel");
+#if AVPRO_FEATURE_VIDEORESOLVE
+			_propUseVideoResolve = this.CheckFindProperty("_useVideoResolve");
+			_propVideoResolve = this.CheckFindProperty("_videoResolve");
+			_propVideoResolveOptions = this.CheckFindProperty("_videoResolveOptions");
+#endif
+			_propVideoMapping = this.CheckFindProperty("_videoMapping");
+			_propForceFileFormat = this.CheckFindProperty("_forceFileFormat");
+			_propFallbackMediaHints = this.CheckFindProperty("_fallbackMediaHints");
+			_propSubtitles = this.CheckFindProperty("_sideloadSubtitles");
+			_propSubtitlePath = this.CheckFindProperty("_subtitlePath");
+			_propResample = this.CheckFindProperty("_useResampler");
+			_propResampleMode = this.CheckFindProperty("_resampleMode");
+			_propResampleBufferSize = this.CheckFindProperty("_resampleBufferSize");
+			_propAudioHeadTransform = this.CheckFindProperty("_audioHeadTransform");
+			_propAudioEnableFocus = this.CheckFindProperty("_audioFocusEnabled");
+			_propAudioFocusOffLevelDB = this.CheckFindProperty("_audioFocusOffLevelDB");
+			_propAudioFocusWidthDegrees = this.CheckFindProperty("_audioFocusWidthDegrees");
+			_propAudioFocusTransform = this.CheckFindProperty("_audioFocusTransform");
+		}
+
+		private static Texture GetPlatformIcon(Platform platform)
+		{
+			string iconName = string.Empty;
+			switch (platform)
+			{
+				case Platform.Windows:
+				case Platform.MacOSX:
+					iconName = "BuildSettings.Standalone.Small";
+					break;
+				case Platform.Android:
+					iconName = "BuildSettings.Android.Small";
+					break;
+				case Platform.iOS:
+					iconName = "BuildSettings.iPhone.Small";
+					break;
+				case Platform.tvOS:
+					iconName = "BuildSettings.tvOS.Small";
+					break;
+				case Platform.WindowsUWP:
+					iconName = "BuildSettings.Metro.Small";
+					break;
+				case Platform.WebGL:
+					iconName = "BuildSettings.WebGL.Small";
+					break;
+			}
+			Texture iconTexture = null;
+			if (!string.IsNullOrEmpty(iconName))
+			{
+				iconTexture = EditorGUIUtility.IconContent(iconName).image;
+			}
+			return iconTexture;
+		}
+
+		private static GUIContent GetPlatformButtonContent(Platform platform)
+		{
+			return new GUIContent(Helper.GetPlatformName(platform), GetPlatformIcon(platform));
+		}
+
+		private void FixRogueEditorBug()
+		{
+			// NOTE: There seems to be a bug in Unity where the editor script will call OnEnable and OnDisable twice.
+			// This is resolved by setting the Window Layout mode to Default.
+			// It causes a problem (at least in Unity 2020.1.11) where the System.Action invocations (usd by AnimCollapseSection)
+			// seem to be in a different 'this' context and so their pointers to serializedObject is not the same, resulting in 
+			// properties modified not marking the serialisedObject as dirty.  To get around this issue we use this static bool
+			// so that OnEnable can only be called once.
+			// https://answers.unity.com/questions/1216599/custom-editor-gets-created-multiple-times-and-rece.html
+			var remainingBuggedEditors  = FindObjectsOfType<MediaPlayerEditor>();
+			foreach(var editor in remainingBuggedEditors)
+			{
+				if (editor == this)
+				{
+					continue;
+				}
+				DestroyImmediate(editor);
+			}
+		}
+
+		private void OnEnable()
+		{
+			FixRogueEditorBug();
+
+			CreateSections();
+
+			LoadSettings();
+
+			_isTrialVersion = IsTrialVersion();
+
+			if (_platformNames == null)
+			{
+				_platformNames = new GUIContent[]
+				{
+					GetPlatformButtonContent(Platform.Windows),
+					GetPlatformButtonContent(Platform.MacOSX),
+					GetPlatformButtonContent(Platform.iOS),
+					GetPlatformButtonContent(Platform.tvOS),
+					GetPlatformButtonContent(Platform.Android),
+					GetPlatformButtonContent(Platform.WindowsUWP),
+					GetPlatformButtonContent(Platform.WebGL)
+				};
+			}
+
+			ResolveProperties();
+		}
+
+		private void OnDisable()
+		{
+			ClosePreview();
+			SaveSettings();
+
+			if (!Application.isPlaying)
+			{
+				// NOTE: For some reason when transitioning into Play mode, Dispose() is not called in the MediaPlayer if
+				// it was playing before the transition because all members are reset to null.  So we must force this
+				// dispose of all resources to handle this case.
+				// Sadly it means we can't keep persistent playback in the inspector when it loses focus, but
+				// hopefully we can find a way to achieve this in the future
+				/*if (EditorApplication.isPlayingOrWillChangePlaymode)
+				{
+					// NOTE: This seems to work for the above issue, but
+					// we'd need to move it to the global event for when the play state changes
+					MediaPlayer.EditorAllPlayersDispose();
+				}*/
+				foreach (MediaPlayer player in this.targets)
+				{
+					player.ForceDispose();
+				}
+			}
+		}
+
+		private void CreateStyles()
+		{
+			if (_styleSectionBox == null)
+			{
+				_styleSectionBox = new GUIStyle(GUI.skin.box);
+				if (!EditorGUIUtility.isProSkin)
+				{
+					_styleSectionBox = new GUIStyle(GUI.skin.box);
+					//_styleSectionBox.normal.background = Texture2D.redTexture;
+				}
+			}
+
+			_iconPlayButton = EditorGUIUtility.IconContent("d_PlayButton");
+			_iconPauseButton = EditorGUIUtility.IconContent("d_PauseButton");
+			_iconSceneViewAudio = EditorGUIUtility.IconContent("d_SceneViewAudio");
+			_iconProject = EditorGUIUtility.IconContent("d_Project");
+			_iconRotateTool = EditorGUIUtility.IconContent("d_RotateTool");
+
+			AnimCollapseSection.CreateStyles();
+		}
+
+		public override void OnInspectorGUI()
+		{
+			MediaPlayer media = (this.target) as MediaPlayer;
+
+			// NOTE: It is important that serializedObject.Update() is called before media.EditorUpdate()
+			// as otherwise the serializedPropertys are not correctly detected as modified
+			serializedObject.Update();
+
+#if AVPROVIDEO_SUPPORT_LIVEEDITMODE
+			bool isPlayingInEditor = false;
+			// Update only during the layout event so that nothing updates for the render event
+			if (!Application.isPlaying && Event.current.type == EventType.Layout)
+			{
+				isPlayingInEditor = media.EditorUpdate();
+			}
+#endif
+
+			if (media == null || _propMediaPath == null)
+			{
+				return;
+			}
+
+			CreateStyles();
+
+			_icon = GetIcon(_icon);
+
+			ShowImportantMessages();
+
+			if (media != null)
+			{
+				OnInspectorGUI_Player(media, media.TextureProducer);
+			}
+
+			AnimCollapseSection.Show(_sectionMediaInfo);
+			if (_allowDeveloperMode)
+			{
+				AnimCollapseSection.Show(_sectionDebug);
+			}
+			AnimCollapseSection.Show(_sectionSettings);
+
+			if (serializedObject.ApplyModifiedProperties())
+			{
+				EditorUtility.SetDirty(target);
+			}
+
+			AnimCollapseSection.Show(_sectionAboutHelp);
+#if AVPROVIDEO_SUPPORT_LIVEEDITMODE
+			if (isPlayingInEditor)
+			{
+				GL.InvalidateState();
+				// NOTE: there seems to be a bug in Unity (2019.3.13) where if you don't have 
+				// GL.sRGBWrite = true and then call RepaintAllViews() it makes the current Inspector
+				// background turn black.  This only happens when using D3D12
+				// UPDATE: this is happening in Unity 2019.4.15 as well, and in D3D11 mode.  It only
+				// happens when loading a video via the Recent Menu.
+
+				bool originalSRGBWrite = GL.sRGBWrite;
+				GL.sRGBWrite = true;
+				//this.Repaint();
+				UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
+				GL.sRGBWrite = originalSRGBWrite;
+			}
+			// TODO: OnDisable - stop the video if it's playing (and unload it?)
+#endif
+		}
+
+		private void OnInspectorGUI_Settings()
+		{
+			foreach (AnimCollapseSection section in _settingSections)
+			{
+				AnimCollapseSection.Show(section, indentLevel:1);
+			}
+		}
+
+		private void ShowSupportWindowButton()
+		{
+			//GUI.backgroundColor = new Color(0.96f, 0.25f, 0.47f);
+			//if (GUILayout.Button("◄ AVPro Video ►\nHelp & Support"))
+			if (GUILayout.Button("Click here for \nHelp & Support"))
+			{
+				SupportWindow.Init();
+			}
+			//GUI.backgroundColor = Color.white;
+		}
+
+		private void ShowImportantMessages()
+		{
+			// Describe the watermark for trial version
+			if (_isTrialVersion)
+			{
+				string message = string.Empty;
+				if (Application.isPlaying)
+				{
+				#if UNITY_EDITOR_WIN
+					MediaPlayer media = (this.target) as MediaPlayer;
+					message = "The watermark is the horizontal bar that moves vertically and the small 'AVPRO TRIAL' text.";
+					if (media.Info != null && media.Info.GetPlayerDescription().Contains("MF-MediaEngine-Hardware"))
+					{
+						message = "The watermark is the RenderHeads logo that moves around the image.";
+					}
+				#elif UNITY_EDITOR_OSX
+					message = "The RenderHeads logo is the watermark.";
+				#endif
+				}
+
+				EditorHelper.IMGUI.BeginWarningTextBox("AVPRO VIDEO - TRIAL WATERMARK", message, Color.yellow, Color.yellow, Color.white);
+				if (GUILayout.Button("Purchase"))
+				{
+					Application.OpenURL(LinkPurchase);
+				}
+				EditorHelper.IMGUI.EndWarningTextBox();
+			}
+
+			// Warning about not using multi-threaded rendering
+			{
+				bool showWarningMT = false;
+
+				if (/*EditorUserBuildSettings.selectedBuildTargetGroup == BuildTargetGroup.iOS ||
+					EditorUserBuildSettings.selectedBuildTargetGroup == BuildTargetGroup.tvOS ||*/
+					EditorUserBuildSettings.selectedBuildTargetGroup == BuildTargetGroup.Android)
+				{
+#if UNITY_2017_2_OR_NEWER
+					showWarningMT = !UnityEditor.PlayerSettings.GetMobileMTRendering(BuildTargetGroup.Android);
+#else
+					showWarningMT = !UnityEditor.PlayerSettings.mobileMTRendering;
+#endif
+				}
+				/*if (EditorUserBuildSettings.selectedBuildTargetGroup == BuildTargetGroup.WSA)
+				{
+				}*/
+				if (showWarningMT)
+				{
+					EditorHelper.IMGUI.WarningTextBox("Performance Warning", "Deploying to Android with multi-threaded rendering disabled is not recommended.  Enable multi-threaded rendering in the Player Settings > Other Settings panel.", Color.yellow, Color.yellow, Color.white);
+				}
+			}
+
+#if !UNITY_2019_3_OR_NEWER
+			if (SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.Direct3D12)
+			{
+				EditorHelper.IMGUI.WarningTextBox("Compatibility Warning", "Direct3D 12 is not supported until Unity 2019.3", Color.yellow, Color.yellow, Color.white);
+			}
+#endif
+			// Warn about using Vulkan graphics API
+#if UNITY_2018_1_OR_NEWER
+			{
+				if (EditorUserBuildSettings.selectedBuildTargetGroup == BuildTargetGroup.Android)
+				{
+					bool showWarningVulkan = false;
+					if (!UnityEditor.PlayerSettings.GetUseDefaultGraphicsAPIs(BuildTarget.Android))
+					{
+						UnityEngine.Rendering.GraphicsDeviceType[] devices = UnityEditor.PlayerSettings.GetGraphicsAPIs(BuildTarget.Android);
+						foreach (UnityEngine.Rendering.GraphicsDeviceType device in devices)
+						{
+							if (device == UnityEngine.Rendering.GraphicsDeviceType.Vulkan)
+							{
+								showWarningVulkan = true;
+								break;
+							}
+						}
+					}
+					if (showWarningVulkan)
+					{
+						EditorHelper.IMGUI.WarningTextBox("Compatibility Warning", "Vulkan graphics API is not supported.  Please go to Player Settings > Android > Auto Graphics API and remove Vulkan from the list.  Only OpenGL 2.0 and 3.0 are supported on Android.", Color.yellow, Color.yellow, Color.white);
+					}
+				}
+			}
+#endif
+		}
+
+		private void OnInspectorGUI_Main()
+		{
+			/////////////////// STARTUP FIELDS
+
+			EditorGUILayout.BeginVertical(_styleSectionBox);
+			GUILayout.Label("Startup", EditorStyles.boldLabel);
+			EditorGUILayout.PropertyField(_propAutoOpen);
+			EditorGUILayout.PropertyField(_propAutoStart, new GUIContent("Auto Play"));
+			EditorGUILayout.EndVertical();
+
+			/////////////////// PLAYBACK FIELDS
+
+			EditorGUILayout.BeginVertical(GUI.skin.box);
+			GUILayout.Label("Playback", EditorStyles.boldLabel);
+
+			EditorGUI.BeginChangeCheck();
+			EditorGUILayout.PropertyField(_propLoop);
+			if (EditorGUI.EndChangeCheck())
+			{
+				Undo.RecordObject(target, "Loop");
+				foreach (MediaPlayer player in this.targets)
+				{
+					player.Loop = _propLoop.boolValue;
+				}
+			}
+
+			EditorGUI.BeginChangeCheck();
+			EditorGUILayout.PropertyField(_propRate);
+			if (EditorGUI.EndChangeCheck())
+			{
+				Undo.RecordObject(target, "PlaybackRate");
+				foreach (MediaPlayer player in this.targets)
+				{
+					player.PlaybackRate = _propRate.floatValue;
+				}
+			}
+
+			EditorGUILayout.EndVertical();
+
+			EditorGUILayout.BeginVertical(GUI.skin.box);
+			GUILayout.Label("Other", EditorStyles.boldLabel);
+			EditorGUILayout.PropertyField(_propPersistent, new GUIContent("Persistent", "Use DontDestroyOnLoad so this object isn't destroyed between level loads"));
+
+			if (_propForceFileFormat != null)
+			{
+				GUIContent label = new GUIContent("Force File Format", "Override automatic format detection when using non-standard file extensions");
+				_propForceFileFormat.enumValueIndex = EditorGUILayout.Popup(label, _propForceFileFormat.enumValueIndex, _fileFormatGuiNames);
+			}
+
+			EditorGUILayout.EndVertical();
+		}
+
+		private void OnInspectorGUI_Visual()
+		{
+#if AVPRO_FEATURE_VIDEORESOLVE
+			
+			{
+				EditorGUILayout.BeginVertical(GUI.skin.box);
+				GUILayout.Label("Resolve", EditorStyles.boldLabel);
+
+				EditorGUI.BeginChangeCheck();
+				EditorGUILayout.PropertyField(_propUseVideoResolve);
+				if (EditorGUI.EndChangeCheck())
+				{
+					Undo.RecordObject(target, "UseVideoResolve");
+					foreach (MediaPlayer player in this.targets)
+					{
+						player.UseVideoResolve = _propUseVideoResolve.boolValue;
+					}
+				}
+
+				if (_propUseVideoResolve.boolValue)
+				{
+					EditorGUILayout.PropertyField(_propVideoResolve);
+					/*EditorGUI.indentLevel++;
+					EditorGUILayout.PropertyField(_propVideoResolveOptions, true);
+					EditorGUI.indentLevel--;*/
+				}
+
+				GUILayout.EndVertical();
+			}
+#endif
+
+			EditorGUILayout.BeginVertical(GUI.skin.box);
+			GUILayout.Label("Texture", EditorStyles.boldLabel);
+
+			EditorGUI.BeginChangeCheck();
+			EditorGUILayout.PropertyField(_propFilter, new GUIContent("Filter"));
+			if (EditorGUI.EndChangeCheck())
+			{
+				Undo.RecordObject(target, "TextureFilterMode");
+				foreach (MediaPlayer player in this.targets)
+				{
+					player.TextureFilterMode = (FilterMode)_propFilter.enumValueIndex;
+				}
+			}
+
+			EditorGUI.BeginChangeCheck();
+			EditorGUILayout.PropertyField(_propWrap, new GUIContent("Wrap"));
+			if (EditorGUI.EndChangeCheck())
+			{
+				Undo.RecordObject(target, "TextureWrapMode");
+				foreach (MediaPlayer player in this.targets)
+				{
+					player.TextureWrapMode = (TextureWrapMode)_propWrap.enumValueIndex;
+				}
+			}
+
+			EditorGUI.BeginChangeCheck();
+			EditorGUILayout.PropertyField(_propAniso, new GUIContent("Aniso"));
+			if (EditorGUI.EndChangeCheck())
+			{
+				Undo.RecordObject(target, "TextureAnisoLevel");
+				foreach (MediaPlayer player in this.targets)
+				{
+					player.TextureAnisoLevel = _propAniso.intValue;
+				}
+			}
+
+			EditorGUILayout.EndVertical();
+
+			EditorGUILayout.BeginVertical(GUI.skin.box);
+			GUILayout.Label("Layout Mapping", EditorStyles.boldLabel);
+			EditorGUILayout.PropertyField(_propVideoMapping);
+			EditorGUILayout.EndVertical();
+
+			{
+				EditorGUILayout.BeginVertical(GUI.skin.box);
+				GUILayout.Label("Resampler (BETA)", EditorStyles.boldLabel);
+				EditorGUILayout.PropertyField(_propResample);
+				EditorGUI.BeginDisabledGroup(!_propResample.boolValue);
+
+				EditorGUILayout.PropertyField(_propResampleMode);
+				EditorGUILayout.PropertyField(_propResampleBufferSize);
+
+				EditorGUI.EndDisabledGroup();
+				EditorGUILayout.EndVertical();
+			}
+		}
+
+		private static bool IsTrialVersion()
+		{
+			string version = GetPluginVersion();
+			return version.Contains("-trial");
+		}
+
+		//private int _updateFrameCount = -1;
+		public override bool RequiresConstantRepaint()
+		{
+			MediaPlayer media = (this.target) as MediaPlayer;
+			if (media != null && media.Control != null && media.isActiveAndEnabled && media.Info.GetDuration() > 0.0)
+			{
+				if (!media.Info.HasVideo())
+				{
+					if (media.Info.HasAudio())
+					{
+						return true;
+					}
+				}
+				else if (media.TextureProducer.GetTexture() != null)
+				{
+					//int frameCount = media.TextureProducer.GetTextureFrameCount();
+					//if (_updateFrameCount != frameCount)
+					{
+						//_updateFrameCount = frameCount;
+						return true;
+					}
+				}
+			}
+			return false;
+		}
+	}
+}

+ 12 - 0
package/Editor/Scripts/Components/MediaPlayerEditor.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 8fdcfef6a9f4f724486d3374e03f4864
+timeCreated: 1448902492
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 160 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_AboutHelp.cs

@@ -0,0 +1,160 @@
+using UnityEngine;
+using UnityEditor;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// About/Help section of the editor for the MediaPlayer component
+	/// </summary>
+	public partial class MediaPlayerEditor : UnityEditor.Editor
+	{
+		public const string LinkPluginWebsite = "https://renderheads.com/products/avpro-video/";
+		public const string LinkForumPage = "https://forum.unity.com/threads/released-avpro-video-complete-video-playback-solution.385611/";
+		public const string LinkForumLastPage = "https://forum.unity.com/threads/released-avpro-video-complete-video-playback-solution.385611/page-100";
+		public const string LinkGithubIssues = "https://github.com/RenderHeads/UnityPlugin-AVProVideo/issues";
+		public const string LinkGithubIssuesNew = "https://github.com/RenderHeads/UnityPlugin-AVProVideo/issues/new/choose";
+		public const string LinkAssetStorePage = "https://assetstore.unity.com/packages/slug/181844?aid=1101lcNgx";
+		public const string LinkUserManual = "https://www.renderheads.com/content/docs/AVProVideo/";
+		public const string LinkScriptingClassReference = "https://www.renderheads.com/content/docs/AVProVideo/api/RenderHeads.Media.AVProVideo.html";
+		public const string LinkPurchase = "https://www.renderheads.com/content/docs/AVProVideo/articles/download.html";
+
+		private struct Native
+		{
+#if UNITY_EDITOR_WIN
+			[System.Runtime.InteropServices.DllImport("AVProVideo")]
+			public static extern System.IntPtr GetPluginVersion();
+#elif UNITY_EDITOR_OSX
+			[System.Runtime.InteropServices.DllImport("AVProVideo")]
+			public static extern System.IntPtr AVPPluginGetVersionStringPointer();
+#endif
+		}
+
+		private static string GetPluginVersion()
+		{
+			string version = "Unknown";
+			try
+			{
+#if UNITY_EDITOR_WIN
+				version = System.Runtime.InteropServices.Marshal.PtrToStringAnsi(Native.GetPluginVersion());
+#elif UNITY_EDITOR_OSX
+				version = System.Runtime.InteropServices.Marshal.PtrToStringAnsi(Native.AVPPluginGetVersionStringPointer());
+#endif
+			}
+			catch (System.DllNotFoundException e)
+			{
+#if UNITY_EDITOR_OSX
+				Debug.LogError("[AVProVideo] Failed to load Bundle. " + e.Message);
+#else
+				Debug.LogError("[AVProVideo] Failed to load DLL. " + e.Message);
+#endif
+			}
+			return version;
+		}
+
+		private static Texture2D GetIcon(Texture2D icon)
+		{
+			if (icon == null)
+			{
+				icon = Resources.Load<Texture2D>("AVProVideoIcon");
+			}
+			return icon;
+		}
+
+		private void OnInspectorGUI_About()
+		{
+			EditorGUILayout.BeginHorizontal();
+			GUILayout.FlexibleSpace();
+			_icon = GetIcon(_icon);
+			if (_icon != null)
+			{
+				GUILayout.Label(new GUIContent(_icon));
+			}
+			GUILayout.FlexibleSpace();
+			EditorGUILayout.EndHorizontal();
+
+			GUI.color = Color.yellow;
+			EditorHelper.IMGUI.CentreLabel("AVPro Video by RenderHeads Ltd", EditorStyles.boldLabel);
+			EditorHelper.IMGUI.CentreLabel("version " + Helper.AVProVideoVersion + " (plugin v" + GetPluginVersion() + ")");
+			GUI.color = Color.white;
+			GUI.backgroundColor = Color.white;
+
+			if (_icon != null)
+			{
+				GUILayout.Space(8f);
+				ShowSupportWindowButton();
+				GUILayout.Space(8f);
+			}
+
+			EditorGUILayout.LabelField("Links", EditorStyles.boldLabel);
+
+			GUILayout.Space(8f);
+
+			EditorGUILayout.LabelField("Documentation");
+			if (GUILayout.Button("User Manual, FAQ, Release Notes", GUILayout.ExpandWidth(false)))
+			{
+				Application.OpenURL(LinkUserManual);
+			}
+			if (GUILayout.Button("Scripting Class Reference", GUILayout.ExpandWidth(false)))
+			{
+				Application.OpenURL(LinkScriptingClassReference);
+			}
+
+			GUILayout.Space(16f);
+
+			GUILayout.Label("Bugs and Support");
+			if (GUILayout.Button("Open Help & Support", GUILayout.ExpandWidth(false)))
+			{
+				SupportWindow.Init();
+			}
+
+			GUILayout.Space(16f);
+
+			GUILayout.Label("Rate and Review (★★★★☆)", GUILayout.ExpandWidth(false));
+			if (GUILayout.Button("Asset Store Page", GUILayout.ExpandWidth(false)))
+			{
+				Application.OpenURL(LinkAssetStorePage);
+			}
+
+			GUILayout.Space(16f);
+
+			GUILayout.Label("Community");
+			if (GUILayout.Button("Forum Thread", GUILayout.ExpandWidth(false)))
+			{
+				Application.OpenURL(LinkForumPage);
+			}
+
+			GUILayout.Space(16f);
+
+			GUILayout.Label("Homepage", GUILayout.ExpandWidth(false));
+			if (GUILayout.Button("Official Website", GUILayout.ExpandWidth(false)))
+			{
+				Application.OpenURL(LinkPluginWebsite);
+			}
+
+			GUILayout.Space(32f);
+
+			EditorGUILayout.LabelField("Credits", EditorStyles.boldLabel);
+			GUILayout.Space(8f);
+
+			EditorHelper.IMGUI.CentreLabel("Programming", EditorStyles.boldLabel);
+			EditorHelper.IMGUI.CentreLabel("Andrew Griffiths");
+			EditorHelper.IMGUI.CentreLabel("Morris Butler");
+			EditorHelper.IMGUI.CentreLabel("Ste Butcher");
+			EditorHelper.IMGUI.CentreLabel("Richard Turnbull");
+			EditorHelper.IMGUI.CentreLabel("Sunrise Wang");
+			EditorHelper.IMGUI.CentreLabel("Muano Mainganye");
+			EditorHelper.IMGUI.CentreLabel("Shane Marks");
+			GUILayout.Space(8f);
+			EditorHelper.IMGUI.CentreLabel("Graphics", EditorStyles.boldLabel);
+			GUILayout.Space(8f);
+			EditorHelper.IMGUI.CentreLabel("Jeff Rusch");
+			EditorHelper.IMGUI.CentreLabel("Luke Godward");
+
+			GUILayout.Space(32f);
+		}
+	}
+}

+ 12 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_AboutHelp.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 2463176874e32294998504d6b1f2f21c
+timeCreated: 1448902492
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 185 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_Android.cs

@@ -0,0 +1,185 @@
+using UnityEngine;
+using UnityEditor;
+using System.Collections.Generic;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// Editor for the MediaPlayer component
+	/// </summary>
+	public partial class MediaPlayerEditor : UnityEditor.Editor
+	{
+		private readonly static GUIContent[] _audioModesAndroid =
+		{
+			new GUIContent("System Direct"),
+			new GUIContent("Unity"),
+			new GUIContent("Facebook Audio 360", "Initialises player with Facebook Audio 360 support"),
+		};
+
+		private readonly static GUIContent[] _blitTextureFilteringAndroid =
+		{
+			new GUIContent("Point"),
+			new GUIContent("Bilinear"),
+			new GUIContent("Trilinear"),
+		};
+
+		private readonly static FieldDescription _optionFileOffset = new FieldDescription(".fileOffset", GUIContent.none);
+		private readonly static FieldDescription _optionUseFastOesPath = new FieldDescription(".useFastOesPath", new GUIContent("Use OES Rendering", "Enables a faster rendering path using OES textures.  This requires that all rendering in Unity uses special GLSL shaders."));
+		private readonly static FieldDescription _optionBlitTextureFiltering = new FieldDescription(".blitTextureFiltering", new GUIContent("Blit Texture Filtering", "The texture filtering used for the final internal blit."));
+		private readonly static FieldDescription _optionShowPosterFrames = new FieldDescription(".showPosterFrame", new GUIContent("Show Poster Frame", "Allows a paused loaded video to display the initial frame. This uses up decoder resources."));
+		private readonly static FieldDescription _optionPreferSoftwareDecoder = new FieldDescription(".preferSoftwareDecoder", GUIContent.none);
+		private readonly static FieldDescription _optionPreferredMaximumResolution = new FieldDescription("._preferredMaximumResolution", new GUIContent("Preferred Maximum Resolution", "The desired maximum resolution of the video."));
+#if UNITY_2017_2_OR_NEWER
+		private readonly static FieldDescription _optionCustomPreferredMaxResolution = new FieldDescription("._customPreferredMaximumResolution", new GUIContent(" "));
+#endif
+		private readonly static FieldDescription _optionCustomPreferredPeakBitRate = new FieldDescription("._preferredPeakBitRate", new GUIContent("Preferred Peak BitRate", "The desired limit of network bandwidth consumption for playback, set to 0 for no preference."));
+		private readonly static FieldDescription _optionCustomPreferredPeakBitRateUnits = new FieldDescription("._preferredPeakBitRateUnits", new GUIContent());
+
+		private readonly static FieldDescription _optionMinBufferMs = new FieldDescription(".minBufferMs", new GUIContent("Minimum Buffer Ms"));
+		private readonly static FieldDescription _optionMaxBufferMs = new FieldDescription(".maxBufferMs", new GUIContent("Maximum Buffer Ms"));
+		private readonly static FieldDescription _optionBufferForPlaybackMs = new FieldDescription(".bufferForPlaybackMs", new GUIContent("Buffer For Playback Ms"));
+		private readonly static FieldDescription _optionBufferForPlaybackAfterRebufferMs = new FieldDescription(".bufferForPlaybackAfterRebufferMs", new GUIContent("Buffer For Playback After Rebuffer Ms"));
+
+		private void OnInspectorGUI_Override_Android()
+		{
+			//MediaPlayer media = (this.target) as MediaPlayer;
+			//MediaPlayer.OptionsAndroid options = media._optionsAndroid;
+
+			GUILayout.Space(8f);
+
+			string optionsVarName = MediaPlayer.GetPlatformOptionsVariable(Platform.Android);
+
+			{
+				EditorGUILayout.BeginVertical(GUI.skin.box);
+
+				DisplayPlatformOption(optionsVarName, _optionVideoAPI);
+
+				{
+					SerializedProperty propFileOffset = DisplayPlatformOption(optionsVarName, _optionFileOffset);
+					propFileOffset.intValue = Mathf.Max(0, propFileOffset.intValue);
+				}
+
+				{
+					SerializedProperty propUseFastOesPath = DisplayPlatformOption(optionsVarName, _optionUseFastOesPath);
+					if (propUseFastOesPath.boolValue)
+					{
+						EditorHelper.IMGUI.NoticeBox(MessageType.Info, "OES can require special shaders.  Make sure you assign an AVPro Video OES shader to your meshes/materials that need to display video.");
+
+						// PlayerSettings.virtualRealitySupported is deprecated from 2019.3
+#if !UNITY_2019_3_OR_NEWER
+						if (PlayerSettings.virtualRealitySupported)
+#endif
+						{
+							if (PlayerSettings.stereoRenderingPath != StereoRenderingPath.MultiPass)
+							{
+								EditorHelper.IMGUI.NoticeBox(MessageType.Error, "OES only supports multi-pass stereo rendering path, please change in Player Settings.");
+							}
+						}
+
+						EditorHelper.IMGUI.NoticeBox(MessageType.Warning, "OES is not supported in the trial version.  If your Android plugin is not trial then you can ignore this warning.");
+					}
+				}
+
+				{
+					SerializedProperty propBlitTextureFiltering = DisplayPlatformOptionEnum(optionsVarName, _optionBlitTextureFiltering, _blitTextureFilteringAndroid);
+					propBlitTextureFiltering.intValue = Mathf.Max(0, propBlitTextureFiltering.intValue);
+				}
+
+				EditorGUILayout.EndVertical();
+			}
+
+			if (_showUltraOptions)
+			{
+				SerializedProperty httpHeadersProp = serializedObject.FindProperty(optionsVarName + ".httpHeaders.httpHeaders");
+				if (httpHeadersProp != null)
+				{
+					OnInspectorGUI_HttpHeaders(httpHeadersProp);
+				}
+
+				SerializedProperty keyAuthProp = serializedObject.FindProperty(optionsVarName + ".keyAuth");
+				if (keyAuthProp != null)
+				{
+					OnInspectorGUI_HlsDecryption(keyAuthProp);
+				}
+			}
+
+			// MediaPlayer API options
+			{
+				EditorGUILayout.BeginVertical(GUI.skin.box);
+				GUILayout.Label("MediaPlayer API Options", EditorStyles.boldLabel);
+
+				DisplayPlatformOption(optionsVarName, _optionShowPosterFrames);
+
+				EditorGUILayout.EndVertical();
+			}
+
+			// ExoPlayer API options
+			{
+				EditorGUILayout.BeginVertical(GUI.skin.box);
+				GUILayout.Label("ExoPlayer API Options", EditorStyles.boldLabel);
+
+				DisplayPlatformOption(optionsVarName, _optionPreferSoftwareDecoder);
+
+				// Audio
+				{
+					SerializedProperty propAudioOutput = DisplayPlatformOptionEnum(optionsVarName, _optionAudioOutput, _audioModesAndroid);
+					if ((Android.AudioOutput)propAudioOutput.enumValueIndex == Android.AudioOutput.FacebookAudio360)
+					{
+						if (_showUltraOptions)
+						{
+							EditorGUILayout.Space();
+							EditorGUILayout.LabelField("Facebook Audio 360", EditorStyles.boldLabel);
+							DisplayPlatformOptionEnum(optionsVarName, _optionAudio360ChannelMode, _audio360ChannelMapGuiNames);
+						}
+					}
+				}
+
+				GUILayout.Space(8f);
+
+//				EditorGUILayout.BeginVertical();
+				EditorGUILayout.LabelField("Adaptive Stream", EditorStyles.boldLabel);
+
+				DisplayPlatformOption(optionsVarName, _optionStartMaxBitrate);
+
+				{
+					SerializedProperty preferredMaximumResolutionProp = DisplayPlatformOption(optionsVarName, _optionPreferredMaximumResolution);
+					if ((MediaPlayer.OptionsAndroid.Resolution)preferredMaximumResolutionProp.intValue == MediaPlayer.OptionsAndroid.Resolution.Custom)
+					{
+#if UNITY_2017_2_OR_NEWER
+						DisplayPlatformOption(optionsVarName, _optionCustomPreferredMaxResolution);
+#endif
+					}
+				}
+
+				{
+					EditorGUILayout.BeginHorizontal();
+					DisplayPlatformOption(optionsVarName, _optionCustomPreferredPeakBitRate);
+					DisplayPlatformOption(optionsVarName, _optionCustomPreferredPeakBitRateUnits);
+					EditorGUILayout.EndHorizontal();
+				}
+
+				DisplayPlatformOption(optionsVarName, _optionMinBufferMs);
+				DisplayPlatformOption(optionsVarName, _optionMaxBufferMs);
+				DisplayPlatformOption(optionsVarName, _optionBufferForPlaybackMs);
+				DisplayPlatformOption(optionsVarName, _optionBufferForPlaybackAfterRebufferMs);
+
+				EditorGUILayout.EndVertical();
+			}
+			GUI.enabled = true;
+
+			/*
+			SerializedProperty propFileOffsetLow = serializedObject.FindProperty(optionsVarName + ".fileOffsetLow");
+			SerializedProperty propFileOffsetHigh = serializedObject.FindProperty(optionsVarName + ".fileOffsetHigh");
+			if (propFileOffsetLow != null && propFileOffsetHigh != null)
+			{
+				propFileOffsetLow.intValue = ;
+
+				EditorGUILayout.PropertyField(propFileOFfset);
+			}*/
+		}
+	}
+}

+ 12 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_Android.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 0516948b5fec81a4eb1566ebd6d4027a
+timeCreated: 1448902492
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 140 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_Apple.cs

@@ -0,0 +1,140 @@
+using UnityEngine;
+using UnityEditor;
+using System.Collections.Generic;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// Editor for the MediaPlayer component
+	/// </summary>
+	public partial class MediaPlayerEditor : UnityEditor.Editor
+	{
+		private readonly static FieldDescription _optionAudioMode = new FieldDescription("._audioMode", new GUIContent("Audio Mode", "Unity mode does not work with HLS video"));
+		private readonly static FieldDescription _optionTextureFormat = new FieldDescription(".textureFormat", new GUIContent("Texture Format", "BGRA32 is the most compatible.\nYCbCr420 uses ~50% of the memory of BGRA32 and has slightly better performance however it does require shader support, recommended for iOS and tvOS."));
+		private readonly static FieldDescription _optionPreferredForwardBufferDuration = new FieldDescription("._preferredForwardBufferDuration", new GUIContent("Preferred Forward Buffer Duration", "The duration in seconds the player should buffer ahead of the playhead to prevent stalling. Set to 0 to let the system decide."));
+		private readonly static FieldDescription _optionCustomPreferredPeakBitRateApple = new FieldDescription("._preferredPeakBitRate", new GUIContent("Preferred Peak BitRate", "The desired limit of network bandwidth consumption for playback, set to 0 for no preference."));
+		private readonly static FieldDescription _optionCustomPreferredPeakBitRateUnitsApple = new FieldDescription("._preferredPeakBitRateUnits", new GUIContent());
+
+		private void OnInspectorGUI_Override_Apple(Platform platform)
+		{
+			GUILayout.Space(8f);
+
+			string optionsVarName = MediaPlayer.GetPlatformOptionsVariable(platform);
+
+			EditorGUILayout.BeginVertical(GUI.skin.box);
+
+			DisplayPlatformOption(optionsVarName, _optionTextureFormat);
+
+			SerializedProperty flagsProp = serializedObject.FindProperty(optionsVarName + "._flags");
+			MediaPlayer.OptionsApple.Flags flags = flagsProp != null ? (MediaPlayer.OptionsApple.Flags)flagsProp.intValue : 0;
+
+			// Texture flags
+			if (flagsProp != null)
+			{
+				bool generateMipmaps = flags.GenerateMipmaps();
+				generateMipmaps = EditorGUILayout.Toggle(new GUIContent("Generate Mipmaps"), generateMipmaps);
+				flags = flags.SetGenerateMipMaps(generateMipmaps);
+			}
+
+			// Audio
+			DisplayPlatformOption(optionsVarName, _optionAudioMode);
+
+			// Platform specific flags
+			if (flagsProp != null)
+			{
+				if (platform == Platform.MacOSX || platform == Platform.iOS)
+				{
+					bool b = flags.AllowExternalPlayback();
+					b = EditorGUILayout.Toggle(new GUIContent("Allow External Playback", "Enables support for playback on external devices via AirPlay."), b);
+					flags = flags.SetAllowExternalPlayback(b);
+				}
+
+				if (platform == Platform.iOS)
+				{
+					bool b = flags.ResumePlaybackAfterAudioSessionRouteChange();
+					b = EditorGUILayout.Toggle(new GUIContent("Resume playback after audio route change", "The default behaviour is for playback to pause when the audio route changes, for instance when disconnecting headphones."), b);
+					flags = flags.SetResumePlaybackAfterAudioSessionRouteChange(b);
+				}
+
+				bool playWithoutBuffering = flags.PlayWithoutBuffering();
+				playWithoutBuffering = EditorGUILayout.Toggle(new GUIContent("Play without buffering"), playWithoutBuffering);
+				flags = flags.SetPlayWithoutBuffering(playWithoutBuffering);
+
+				bool useSinglePlayerItem = flags.UseSinglePlayerItem();
+				useSinglePlayerItem = EditorGUILayout.Toggle(new GUIContent("Use single player item", "Restricts the media player to using only one player item. This can help reduce network usage for remote videos but will cause a stall when looping."), useSinglePlayerItem);
+				flags = flags.SetUseSinglePlayerItem(useSinglePlayerItem);
+			}
+
+			SerializedProperty maximumPlaybackRateProp = serializedObject.FindProperty(optionsVarName + ".maximumPlaybackRate");
+			if (maximumPlaybackRateProp != null)
+			{
+				EditorGUILayout.Slider(maximumPlaybackRateProp, 2.0f, 10.0f, new GUIContent("Max Playback Rate", "Increase the maximum playback rate before which playback switches to key-frames only."));
+			}
+
+			GUILayout.Space(8f);
+
+			EditorGUILayout.BeginVertical();
+			EditorGUILayout.LabelField("Network", EditorStyles.boldLabel);
+
+			SerializedProperty preferredMaximumResolutionProp = DisplayPlatformOption(optionsVarName, _optionPreferredMaximumResolution);
+			if ((MediaPlayer.OptionsApple.Resolution)preferredMaximumResolutionProp.intValue == MediaPlayer.OptionsApple.Resolution.Custom)
+			{
+				#if UNITY_2017_2_OR_NEWER
+				DisplayPlatformOption(optionsVarName, _optionCustomPreferredMaxResolution);
+				#endif
+			}
+
+			EditorGUILayout.BeginHorizontal();
+			DisplayPlatformOption(optionsVarName, _optionCustomPreferredPeakBitRateApple);
+			DisplayPlatformOption(optionsVarName, _optionCustomPreferredPeakBitRateUnitsApple);
+			EditorGUILayout.EndHorizontal();
+
+			DisplayPlatformOption(optionsVarName, _optionPreferredForwardBufferDuration);
+
+			EditorGUILayout.EndVertical();
+
+			EditorGUILayout.EndVertical();
+
+			// Set the flags
+
+			if (flagsProp != null)
+			{
+				flagsProp.intValue = (int)flags;
+			}
+
+			if (_showUltraOptions)
+			{
+				SerializedProperty keyAuthProp = serializedObject.FindProperty(optionsVarName + ".keyAuth");
+				if (keyAuthProp != null)
+				{
+					OnInspectorGUI_HlsDecryption(keyAuthProp);
+				}
+
+				SerializedProperty httpHeadersProp = serializedObject.FindProperty(optionsVarName + ".httpHeaders.httpHeaders");
+				if (httpHeadersProp != null)
+				{
+					OnInspectorGUI_HttpHeaders(httpHeadersProp);
+				}
+			}
+		}
+
+		private void OnInspectorGUI_Override_MacOSX()
+		{
+			OnInspectorGUI_Override_Apple(Platform.MacOSX);
+		}
+
+		private void OnInspectorGUI_Override_iOS()
+		{
+			OnInspectorGUI_Override_Apple(Platform.iOS);
+		}
+
+		private void OnInspectorGUI_Override_tvOS()
+		{
+			OnInspectorGUI_Override_Apple(Platform.tvOS);
+		}
+	}
+}

+ 12 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_Apple.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 38bbbff2994464c48b6d633a311b63f6
+timeCreated: 1448902492
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 87 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_Audio.cs

@@ -0,0 +1,87 @@
+using UnityEngine;
+using UnityEditor;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// Editor for the MediaPlayer component
+	/// </summary>
+	public partial class MediaPlayerEditor : UnityEditor.Editor
+	{
+		private SerializedProperty _propVolume;
+		private SerializedProperty _propBalance;
+		private SerializedProperty _propMuted;
+		private SerializedProperty _propAudioHeadTransform;
+		private SerializedProperty _propAudioEnableFocus;
+		private SerializedProperty _propAudioFocusOffLevelDB;
+		private SerializedProperty _propAudioFocusWidthDegrees;
+		private SerializedProperty _propAudioFocusTransform;
+
+		private void OnInspectorGUI_Audio()
+		{
+			if (EditorUtility.audioMasterMute)
+			{
+				EditorGUILayout.BeginHorizontal();
+				EditorGUILayout.HelpBox("Audio is currently muted in Editor", MessageType.Warning, true);
+				if (GUILayout.Button("Unmute", GUILayout.ExpandHeight(true)))
+				{
+					EditorUtility.audioMasterMute = false;
+					UnityEditorInternal.InternalEditorUtility.RepaintAllViews();	// To force the GameView audio mute toggle display state to update
+				}
+				EditorGUILayout.EndHorizontal();
+			}
+			EditorGUILayout.BeginVertical(GUI.skin.box);
+
+			EditorGUI.BeginChangeCheck();
+			EditorGUILayout.PropertyField(_propVolume, new GUIContent("Volume"));
+			if (EditorGUI.EndChangeCheck())
+			{
+				foreach (MediaPlayer player in this.targets)
+				{
+					player.AudioVolume = _propVolume.floatValue;
+				}
+			}
+
+			EditorGUI.BeginChangeCheck();
+			EditorGUILayout.PropertyField(_propBalance, new GUIContent("Balance"));
+			if (EditorGUI.EndChangeCheck())
+			{
+				foreach (MediaPlayer player in this.targets)
+				{
+					player.AudioBalance = _propBalance.floatValue;
+				}
+			}
+
+			EditorGUI.BeginChangeCheck();
+			EditorGUILayout.PropertyField(_propMuted, new GUIContent("Muted"));
+			if (EditorGUI.EndChangeCheck())
+			{
+				foreach (MediaPlayer player in this.targets)
+				{
+					player.AudioMuted = _propMuted.boolValue;
+				}
+			}
+
+			EditorGUILayout.EndVertical();
+
+			if (_showUltraOptions)
+			{
+				EditorGUILayout.BeginVertical(GUI.skin.box);
+				GUILayout.Label("Audio 360", EditorStyles.boldLabel);
+				EditorGUILayout.PropertyField(_propAudioHeadTransform, new GUIContent("Head Transform", "Set this to your head camera transform. Only currently used for Facebook Audio360"));
+				EditorGUILayout.PropertyField(_propAudioEnableFocus, new GUIContent("Enable Focus", "Enables focus control. Only currently used for Facebook Audio360"));
+				if (_propAudioEnableFocus.boolValue)
+				{
+					EditorGUILayout.PropertyField(_propAudioFocusOffLevelDB, new GUIContent("Off Focus Level DB", "Sets the off-focus level in DB, with the range being between -24 to 0 DB. Only currently used for Facebook Audio360"));
+					EditorGUILayout.PropertyField(_propAudioFocusWidthDegrees, new GUIContent("Focus Width Degrees", "Set the focus width in degrees, with the range being between 40 and 120 degrees. Only currently used for Facebook Audio360"));
+					EditorGUILayout.PropertyField(_propAudioFocusTransform, new GUIContent("Focus Transform", "Set this to where you wish to focus on the video. Only currently used for Facebook Audio360"));
+				}
+				EditorGUILayout.EndVertical();
+			}
+		}
+	}
+}

+ 12 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_Audio.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 37646bf6e83e0f5429dc604d9f8b86fc
+timeCreated: 1448902492
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 284 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_Debug.cs

@@ -0,0 +1,284 @@
+using UnityEngine;
+using UnityEditor;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// Editor for the MediaPlayer component
+	/// </summary>
+	public partial class MediaPlayerEditor : UnityEditor.Editor
+	{
+		private static bool _allowDeveloperMode = false;
+		private static bool _showUltraOptions = true;
+
+		private AnimCollapseSection _sectionDevModeState;
+		private AnimCollapseSection _sectionDevModeTexture;
+		private AnimCollapseSection _sectionDevModeHapNotchLCDecoder;
+		private AnimCollapseSection _sectionDevModeBufferedFrames;
+		private AnimCollapseSection _sectionDevModePlaybackQuality;
+
+		private static readonly GUIContent _guiTextMetaData = new GUIContent("MetaData");
+		private static readonly GUIContent _guiTextPaused = new GUIContent("Paused");
+		private static readonly GUIContent _guiTextPlaying = new GUIContent("Playing");
+		private static readonly GUIContent _guiTextSeeking = new GUIContent("Seeking");
+		private static readonly GUIContent _guiTextBuffering = new GUIContent("Buffering");
+		private static readonly GUIContent _guiTextStalled = new GUIContent("Stalled");
+		private static readonly GUIContent _guiTextFinished = new GUIContent("Finished");
+		private static readonly GUIContent _guiTextTimeColon= new GUIContent("Time: ");
+		private static readonly GUIContent _guiTextFrameColon = new GUIContent("Frame: ");
+
+		private static readonly GUIContent _guiTextFrameDec = new GUIContent("<");
+		private static readonly GUIContent _guiTextFrameInc = new GUIContent(">");
+		private static readonly GUIContent _guiTextSelectTexture = new GUIContent("Select Texture");
+		private static readonly GUIContent _guiTextSaveFramePNG = new GUIContent("Save Frame PNG");
+		private static readonly GUIContent _guiTextSaveFrameEXR = new GUIContent("Save Frame EXR");
+
+		private static readonly GUIContent _guiTextDecodeStats = new GUIContent("Decode Stats");
+		private static readonly GUIContent _guiTextParallelFrames = new GUIContent("Parallel Frames");
+		private static readonly GUIContent _guiTextDecodedFrames = new GUIContent("Decoded Frames");
+		private static readonly GUIContent _guiTextDroppedFrames = new GUIContent("Dropped Frames");
+
+		private static readonly GUIContent _guiTextBufferedFrames = new GUIContent("Buffered Frames");
+		private static readonly GUIContent _guiTextFreeFrames = new GUIContent("Free Frames");
+		//private static readonly GUIContent _guiTextDisplayTimestamp = new GUIContent("Display Timstamp");
+		//private static readonly GUIContent _guiTextMinTimestamp = new GUIContent("Min Timstamp");
+		//private static readonly GUIContent _guiTextMaxTimestamp = new GUIContent("Max Timstamp");
+		private static readonly GUIContent _guiTextFlush = new GUIContent("Flush");
+		private static readonly GUIContent _guiTextReset = new GUIContent("Reset");
+
+		private void OnInspectorGUI_DevMode_State()
+		{
+			MediaPlayer mediaPlayer = (this.target) as MediaPlayer;
+			if (mediaPlayer.Control != null)
+			{
+				// State
+
+				GUIStyle style = GUI.skin.button;
+				using (HorizontalFlowScope flow = new HorizontalFlowScope(Screen.width))
+				{
+					flow.AddItem(_guiTextMetaData, style);
+					GUI.color = mediaPlayer.Control.HasMetaData() ? Color.green : Color.white;
+					GUILayout.Toggle(true, _guiTextMetaData, style);
+
+					flow.AddItem(_guiTextPaused, style);
+					GUI.color = mediaPlayer.Control.IsPaused() ? Color.green : Color.white;
+					GUILayout.Toggle(true, _guiTextPaused, style);
+
+					flow.AddItem(_guiTextPlaying, style);
+					GUI.color = mediaPlayer.Control.IsPlaying() ? Color.green : Color.white;
+					GUILayout.Toggle(true, _guiTextPlaying, style);
+
+					flow.AddItem(_guiTextSeeking, style);
+					GUI.color = mediaPlayer.Control.IsSeeking() ? Color.green : Color.white;
+					GUILayout.Toggle(true, _guiTextSeeking, style);
+
+					flow.AddItem(_guiTextBuffering, style);
+					GUI.color = mediaPlayer.Control.IsBuffering() ? Color.green : Color.white;
+					GUILayout.Toggle(true, _guiTextBuffering, style);
+
+					flow.AddItem(_guiTextStalled, style);
+					GUI.color = mediaPlayer.Info.IsPlaybackStalled() ? Color.green : Color.white;
+					GUILayout.Toggle(true, _guiTextStalled, style);
+
+					flow.AddItem(_guiTextFinished, style);
+					GUI.color = mediaPlayer.Control.IsFinished() ? Color.green : Color.white;
+					GUILayout.Toggle(true, _guiTextFinished, style);
+				}
+				GUI.color = Color.white;
+
+				// Time, FPS, Frame stepping
+				GUILayout.BeginHorizontal();
+				GUILayout.Label(_guiTextTimeColon);
+				GUILayout.Label(mediaPlayer.Control.GetCurrentTime().ToString());
+				GUILayout.FlexibleSpace();
+				GUILayout.Label(_guiTextFrameColon);
+				GUILayout.Label(mediaPlayer.Control.GetCurrentTimeFrames().ToString());
+				EditorGUI.BeginDisabledGroup(mediaPlayer.Info.GetVideoFrameRate() <= 0f);
+				if (GUILayout.Button(_guiTextFrameDec))
+				{
+					mediaPlayer.Control.SeekToFrameRelative(-1);
+				}
+				if (GUILayout.Button(_guiTextFrameInc))
+				{
+					mediaPlayer.Control.SeekToFrameRelative(1);
+				}
+				EditorGUI.EndDisabledGroup();
+				GUILayout.EndHorizontal();
+			}
+		}
+
+		private void OnInspectorGUI_DevMode_Texture()
+		{
+			MediaPlayer mediaPlayer = (this.target) as MediaPlayer;
+			if (mediaPlayer.Control != null)
+			{
+				// Raw texture preview
+				if (mediaPlayer.TextureProducer != null)
+				{
+					GUILayout.BeginHorizontal(GUILayout.ExpandWidth(true));
+					GUILayout.FlexibleSpace();
+					for (int i = 0; i < mediaPlayer.TextureProducer.GetTextureCount(); i++)
+					{
+						Texture texture = mediaPlayer.TextureProducer.GetTexture(i);
+						if (texture != null)
+						{
+							GUILayout.BeginVertical();
+							Rect textureRect = GUILayoutUtility.GetRect(128f, 128f);
+							if (Event.current.type == EventType.Repaint)
+							{
+								GUI.color = Color.gray;
+								EditorGUI.DrawTextureTransparent(textureRect, Texture2D.blackTexture, ScaleMode.StretchToFill);
+								GUI.color = Color.white;
+							}
+							GUI.DrawTexture(textureRect, texture, ScaleMode.ScaleToFit, false);
+							GUILayout.Label(texture.width + "x" + texture.height + " ");
+							if (GUILayout.Button(_guiTextSelectTexture, GUILayout.ExpandWidth(false)))
+							{
+								Selection.activeObject = texture;
+							}
+							GUILayout.EndVertical();
+						}
+					}
+					GUILayout.FlexibleSpace();
+					GUILayout.EndHorizontal();
+
+					GUILayout.Label("Updates: " + mediaPlayer.TextureProducer.GetTextureFrameCount());
+					GUILayout.Label("TimeStamp: " + mediaPlayer.TextureProducer.GetTextureTimeStamp());
+
+					GUILayout.BeginHorizontal();
+					if (GUILayout.Button(_guiTextSaveFramePNG, GUILayout.ExpandWidth(true)))
+					{
+						mediaPlayer.SaveFrameToPng();
+					}
+					if (GUILayout.Button(_guiTextSaveFrameEXR, GUILayout.ExpandWidth(true)))
+					{
+						mediaPlayer.SaveFrameToExr();
+					}
+					GUILayout.EndHorizontal();
+				}
+			}
+		}
+
+		private void OnInspectorGUI_DevMode_HapNotchLCDecoder()
+		{
+			MediaPlayer mediaPlayer = (this.target) as MediaPlayer;
+			if (mediaPlayer.Info != null)
+			{
+				int activeDecodeThreadCount = 0;
+				int decodedFrameCount = 0;
+				int droppedFrameCount = 0;
+				if (mediaPlayer.Info.GetDecoderPerformance(ref activeDecodeThreadCount, ref decodedFrameCount, ref droppedFrameCount))
+				{
+					GUILayout.Label(_guiTextDecodeStats);
+					EditorGUI.indentLevel++;
+					EditorGUILayout.Slider(_guiTextParallelFrames, activeDecodeThreadCount, 0f, mediaPlayer.PlatformOptionsWindows.parallelFrameCount);
+					EditorGUILayout.Slider(_guiTextDecodedFrames, decodedFrameCount, 0f, mediaPlayer.PlatformOptionsWindows.prerollFrameCount * 2);
+					EditorGUILayout.IntField(_guiTextDroppedFrames, droppedFrameCount);
+					EditorGUI.indentLevel--;
+				}
+			}
+		}
+
+		private float _lastBufferedFrameCount;
+		private float _lastFreeFrameCount;
+
+		private void OnInspectorGUI_DevMode_BufferedFrames()
+		{
+			MediaPlayer mediaPlayer = (this.target) as MediaPlayer;
+			if (mediaPlayer.Control != null)
+			{
+				IBufferedDisplay bufferedDisplay = mediaPlayer.BufferedDisplay;
+				if (bufferedDisplay != null)
+				{
+					BufferedFramesState state = bufferedDisplay.GetBufferedFramesState();
+
+					GUILayout.BeginHorizontal();
+					EditorGUILayout.PrefixLabel(_guiTextBufferedFrames);
+					Rect progressRect = EditorGUILayout.GetControlRect(false, EditorGUIUtility.singleLineHeight);
+					EditorGUI.ProgressBar(progressRect, _lastBufferedFrameCount, state.bufferedFrameCount.ToString());
+					GUILayout.EndHorizontal();
+
+					GUILayout.BeginHorizontal();
+					EditorGUILayout.PrefixLabel(_guiTextFreeFrames);
+					progressRect = EditorGUILayout.GetControlRect(false, EditorGUIUtility.singleLineHeight);
+					EditorGUI.ProgressBar(progressRect, _lastFreeFrameCount, state.freeFrameCount.ToString());
+					GUILayout.EndHorizontal();
+
+					_lastBufferedFrameCount = Mathf.MoveTowards(_lastBufferedFrameCount, state.bufferedFrameCount / 12f, Time.deltaTime);
+					_lastFreeFrameCount = Mathf.MoveTowards(_lastFreeFrameCount, state.freeFrameCount / 12f, Time.deltaTime);
+
+					//EditorGUILayout.LabelField(_guiTextDisplayTimestamp, new GUIContent(mediaPlayer.TextureProducer.GetTextureTimeStamp().ToString() + " " + (mediaPlayer.TextureProducer.GetTextureTimeStamp() / Helper.SecondsToHNS).ToString() + "s"));
+					//EditorGUILayout.LabelField(_guiTextMinTimestamp, new GUIContent(state.minTimeStamp.ToString() + " " + (state.minTimeStamp / Helper.SecondsToHNS).ToString() + "s"));
+					//EditorGUILayout.LabelField(_guiTextMaxTimestamp, new GUIContent(state.maxTimeStamp.ToString() + " " + (state.maxTimeStamp / Helper.SecondsToHNS).ToString() + "s"));
+					if (GUILayout.Button(_guiTextFlush))
+					{
+						// Seek causes a flush
+						mediaPlayer.Control.Seek(mediaPlayer.Control.GetCurrentTime());
+					}
+				}
+			}
+		}
+
+		private void OnInspectorGUI_DevMode_PresentationQuality()
+		{
+			MediaPlayer mediaPlayer = (this.target) as MediaPlayer;
+			if (mediaPlayer.Info != null)
+			{
+				PlaybackQualityStats stats = mediaPlayer.Info.GetPlaybackQualityStats();
+				//stats.LogIssues = true;
+				stats.LogIssues = EditorGUILayout.Toggle("Log Issues", stats.LogIssues);
+				GUILayout.Label("Video", EditorStyles.boldLabel);
+				EditorGUI.indentLevel++;
+				EditorGUILayout.LabelField("Skipped Frames", stats.SkippedFrames.ToString());
+				GUILayout.BeginHorizontal();
+				EditorGUILayout.LabelField("Duplicate Frames", stats.DuplicateFrames.ToString());
+				GUILayout.Label(stats.VSyncStatus);
+				GUILayout.EndHorizontal();
+				EditorGUILayout.LabelField("Perfect Frames", (stats.PerfectFramesT * 100f).ToString("F2") + "%");
+				EditorGUI.indentLevel--;
+				GUILayout.Label("Unity", EditorStyles.boldLabel);
+				EditorGUI.indentLevel++;
+				EditorGUILayout.LabelField("Dropped Frames", stats.UnityDroppedFrames.ToString());
+				EditorGUI.indentLevel--;
+				if (GUILayout.Button(_guiTextReset))
+				{
+					stats.Reset();
+				}
+			}
+		}
+
+		private void OnInspectorGUI_Debug()
+		{
+			MediaPlayer mediaPlayer = (this.target) as MediaPlayer;
+			IMediaInfo info = mediaPlayer.Info;
+			if (info != null)
+			{
+				AnimCollapseSection.Show(_sectionDevModeState);
+				AnimCollapseSection.Show(_sectionDevModeTexture);
+				AnimCollapseSection.Show(_sectionDevModePlaybackQuality);
+			}
+
+			if (info != null)
+			{
+#if UNITY_EDITOR_WIN
+				if (mediaPlayer.PlatformOptionsWindows.useHapNotchLC)
+				{
+					AnimCollapseSection.Show(_sectionDevModeHapNotchLCDecoder);
+				}
+				if (mediaPlayer.PlatformOptionsWindows.bufferedFrameSelection != BufferedFrameSelectionMode.None)
+				{
+					AnimCollapseSection.Show(_sectionDevModeBufferedFrames);
+				}
+#endif
+			}
+			else
+			{
+				GUILayout.Label("No media loaded");
+			}
+		}
+	}
+}

+ 12 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_Debug.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 8c5649aed6704fa4199ad212f4562fdb
+timeCreated: 1594038897
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 40 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_Events.cs

@@ -0,0 +1,40 @@
+using UnityEngine;
+using UnityEditor;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// Editor for the MediaPlayer component
+	/// </summary>
+	public partial class MediaPlayerEditor : UnityEditor.Editor
+	{
+		private SerializedProperty _propEvents;
+		private SerializedProperty _propEventMask;
+		private SerializedProperty _propPauseMediaOnAppPause;
+		private SerializedProperty _propPlayMediaOnAppUnpause;
+
+		private void OnInspectorGUI_Events()
+		{		
+			EditorGUILayout.BeginVertical(GUI.skin.box);
+			
+			EditorGUILayout.PropertyField(_propEvents);
+
+			_propEventMask.intValue = EditorGUILayout.MaskField("Triggered Events", _propEventMask.intValue, System.Enum.GetNames(typeof(MediaPlayerEvent.EventType)));
+
+			EditorGUILayout.BeginHorizontal();
+			GUILayout.Label("Pause Media On App Pause");
+			_propPauseMediaOnAppPause.boolValue = EditorGUILayout.Toggle(_propPauseMediaOnAppPause.boolValue);
+			EditorGUILayout.EndHorizontal();
+			EditorGUILayout.BeginHorizontal();
+			GUILayout.Label("Play Media On App Unpause");
+			_propPlayMediaOnAppUnpause.boolValue = EditorGUILayout.Toggle(_propPlayMediaOnAppUnpause.boolValue);
+			EditorGUILayout.EndHorizontal();
+
+			EditorGUILayout.EndVertical();
+		}
+	}
+}

+ 12 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_Events.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 191b4e7b3d732b44381d348e9e0dc7ea
+timeCreated: 1448902492
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 78 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_Global.cs

@@ -0,0 +1,78 @@
+using UnityEngine;
+using UnityEditor;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// Editor for the MediaPlayer component
+	/// </summary>
+	public partial class MediaPlayerEditor : UnityEditor.Editor
+	{
+		private void OnInspectorGUI_GlobalSettings()
+		{
+			EditorGUI.BeginDisabledGroup(Application.isPlaying);
+			EditorGUILayout.LabelField("Target Platform", EditorUserBuildSettings.selectedBuildTargetGroup.ToString());
+			if (EditorUserBuildSettings.selectedBuildTargetGroup != BuildTargetGroup.Standalone)
+			{
+				EditorHelper.IMGUI.NoticeBox(MessageType.Warning, "These global options only affect the current target platform so will not apply in-editor unless you change your Build Target and reapply them.");
+			}
+
+			EditorGUILayout.BeginVertical(GUI.skin.box);
+			GUILayout.Label("Video Capture", EditorStyles.boldLabel);
+
+			// TimeScale
+			{
+				const string TimeScaleDefine = "AVPROVIDEO_BETA_SUPPORT_TIMESCALE";
+				if (EditorHelper.IMGUI.ToggleScriptDefine("TimeScale Support", TimeScaleDefine))
+				{
+					EditorHelper.IMGUI.NoticeBox(MessageType.Warning, "This will affect performance if you change Time.timeScale or Time.captureFramerate.  This feature is useful for supporting video capture system that adjust time scale during capturing.");
+				}
+			}
+			EditorGUILayout.EndVertical();
+			
+			EditorGUILayout.BeginVertical(GUI.skin.box);
+			GUILayout.Label("Other", EditorStyles.boldLabel);
+
+			// Disable Logging
+			{
+				const string DisableLogging = "AVPROVIDEO_DISABLE_LOGGING";
+				EditorHelper.IMGUI.ToggleScriptDefine("Disable Logging", DisableLogging);
+			}
+
+			// Show Ultra Options
+			{
+				const string ShowUltraOptions = "AVPROVIDEO_SHOW_ULTRA_OPTIONS";
+				EditorHelper.IMGUI.ToggleScriptDefine("Show Ultra Options", ShowUltraOptions);
+			}
+
+			_allowDeveloperMode = EditorGUILayout.Toggle(new GUIContent("Developer Mode", "Enables some additional information useful for debugging"), _allowDeveloperMode);
+
+			if (_allowDeveloperMode)
+			{
+				EditorGUI.indentLevel++;
+				EditorGUILayout.BeginVertical(GUI.skin.box);
+				GUILayout.Label("BETA / Experimental", EditorStyles.boldLabel);
+
+				// Disable Debug GUI
+				{
+					const string SupportBufferedDisplayDefine = "AVPROVIDEO_SUPPORT_BUFFERED_DISPLAY";
+					if (!EditorHelper.IMGUI.ToggleScriptDefine("Support Buffered Display", SupportBufferedDisplayDefine))
+					{
+						EditorHelper.IMGUI.NoticeBox(MessageType.Info, "The Debug GUI can be disabled globally for builds to help reduce garbage generation each frame.");
+					}
+				}
+
+				EditorGUILayout.EndVertical();
+				EditorGUI.indentLevel--;
+			}
+
+			EditorGUILayout.EndVertical();
+
+			EditorGUI.EndDisabledGroup();
+		}
+	}
+}

+ 12 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_Global.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 43f33634e709c224aa295751513f8f63
+timeCreated: 1448902492
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 130 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_Network.cs

@@ -0,0 +1,130 @@
+using UnityEngine;
+using UnityEditor;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// Editor for the MediaPlayer component
+	/// </summary>
+	public partial class MediaPlayerEditor : UnityEditor.Editor
+	{
+		private void OnInspectorGUI_Network()
+		{
+			if (_showUltraOptions)
+			{
+				SerializedProperty httpHeadersProp = serializedObject.FindProperty("_httpHeaders.httpHeaders");
+				OnInspectorGUI_HttpHeaders(httpHeadersProp);
+
+				SerializedProperty keyAuthProp = serializedObject.FindProperty("_keyAuth");
+				OnInspectorGUI_HlsDecryption(keyAuthProp);
+			}
+		}
+
+		private void OnInspectorGUI_HlsDecryption(SerializedProperty keyAuthProp)
+		{
+			if (keyAuthProp == null) return;
+
+			EditorGUILayout.BeginVertical(GUI.skin.box);
+			GUILayout.Label("HLS Decryption", EditorStyles.boldLabel);
+
+			// Key server auth token
+			SerializedProperty prop = keyAuthProp.FindPropertyRelative("keyServerToken");
+			if (prop != null)
+			{
+				EditorGUILayout.PropertyField(prop, new GUIContent("Auth Token", "Token to pass to the key server in the 'Authorization' HTTP header field"));
+			}
+
+			//GUILayout.Label("Overrides");
+			//EditorGUI.indentLevel++;
+
+			// Key server override
+			/*prop = serializedObject.FindProperty(optionsVarName + ".keyServerURLOverride");
+			if (prop != null)
+			{
+				EditorGUILayout.PropertyField(prop, new GUIContent("Key Server URL", "Overrides the key server URL if present in a HLS manifest."));
+			}*/
+			
+			// Key data blob override
+			prop = keyAuthProp.FindPropertyRelative("overrideDecryptionKeyBase64");
+			if (prop != null)
+			{
+				EditorGUILayout.PropertyField(prop, new GUIContent("Key Override (Base64)", "Override key to use for decoding encrypted HLS streams (in Base64 format)."));
+			}
+
+			//EditorGUI.indentLevel--;
+
+			EditorGUILayout.EndVertical();
+		}
+
+		private void OnInspectorGUI_HttpHeaders(SerializedProperty httpHeadersProp)
+		{
+			if (httpHeadersProp ==  null) return;
+			
+			//GUILayout.Space(8f);
+			bool isExpanded = _HTTPHeadersToggle;
+			if (isExpanded)
+			{
+				EditorGUILayout.BeginVertical(GUI.skin.box);
+			}
+			bool hasHeaders = (httpHeadersProp.arraySize > 0);
+			Color tintColor = hasHeaders?Color.yellow:Color.white;
+			if (AnimCollapseSection.BeginShow("Custom HTTP Headers", ref _HTTPHeadersToggle, tintColor))
+			{
+				{
+					if (httpHeadersProp.arraySize > 0)
+					{
+						int deleteIndex = -1;
+						for (int i = 0; i < httpHeadersProp.arraySize; ++i)
+						{
+							SerializedProperty httpHeaderProp = httpHeadersProp.GetArrayElementAtIndex(i);
+							SerializedProperty headerProp = httpHeaderProp.FindPropertyRelative("name");
+
+							GUILayout.BeginVertical(GUI.skin.box);
+							GUILayout.BeginHorizontal();
+
+							GUI.color = HttpHeader.IsValid(headerProp.stringValue)?Color.white:Color.red;
+							EditorGUILayout.PropertyField(headerProp, GUIContent.none);
+							headerProp.stringValue = headerProp.stringValue.Trim();
+							GUI.color = Color.white;
+
+							if (GUILayout.Button("-", GUILayout.ExpandWidth(false)))
+							{
+								deleteIndex = i;
+							}
+							GUILayout.EndHorizontal();
+
+							SerializedProperty valueProp = httpHeaderProp.FindPropertyRelative("value");
+							GUI.color = HttpHeader.IsValid(valueProp.stringValue)?Color.white:Color.red;
+							valueProp.stringValue = EditorGUILayout.TextArea(valueProp.stringValue, EditorHelper.IMGUI.GetWordWrappedTextAreaStyle());
+							GUI.color = Color.white;
+							valueProp.stringValue = valueProp.stringValue.Trim();
+							GUILayout.EndVertical();
+							GUILayout.Space(4f);
+						}
+
+						if (deleteIndex >= 0)
+						{
+							httpHeadersProp.DeleteArrayElementAtIndex(deleteIndex);
+						}
+					}
+					if (GUILayout.Button("+"))
+					{
+						httpHeadersProp.InsertArrayElementAtIndex(httpHeadersProp.arraySize);
+					}
+				}
+			}
+			AnimCollapseSection.EndShow();
+
+			if (isExpanded)
+			{
+				EditorGUILayout.EndVertical();
+			}
+			
+			//GUILayout.Space(8f);
+		}
+	}
+}

+ 12 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_Network.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: e21d984efff21a1498d41745548e8f14
+timeCreated: 1592503568
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 368 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_Platforms.cs

@@ -0,0 +1,368 @@
+using UnityEngine;
+using UnityEditor;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// Editor for the MediaPlayer component
+	/// </summary>
+	public partial class MediaPlayerEditor : UnityEditor.Editor
+	{
+		private static int _platformIndex = -1;
+		private static bool _HTTPHeadersToggle = false;
+		private static GUIContent[] _platformNames = null;
+
+		private void OnInspectorGUI_SelectPlatform()
+		{
+			// TODO: support multiple targets?
+			MediaPlayer media = (this.target) as MediaPlayer;
+
+			int i = 0;
+			int platformIndex = _platformIndex;
+			foreach (GUIContent platformText in _platformNames)
+			{
+				MediaPlayer.PlatformOptions options = media.GetPlatformOptions((Platform)i);
+
+				Color hilight = Color.yellow;
+
+				if (i == _platformIndex)
+				{
+					// Selected, unmodified
+					if (!options.IsModified())
+					{
+						GUI.contentColor = Color.white;
+					}
+					else
+					{
+						// Selected, modified
+						GUI.color = hilight;
+						GUI.contentColor = Color.white;
+					}
+				}
+				else if (options.IsModified())
+				{
+					// Unselected, modified
+					GUI.backgroundColor = Color.grey* hilight;
+					GUI.contentColor = hilight;
+				}
+				else
+				{
+					// Unselected, unmodified
+					if (EditorGUIUtility.isProSkin)
+					{
+						GUI.backgroundColor = Color.grey;
+						GUI.color = new Color(0.65f, 0.66f, 0.65f);// Color.grey;
+					}
+				}
+
+				if (i == _platformIndex)
+				{
+					if (!GUILayout.Toggle(true, _platformNames[i], GUI.skin.button))
+					{
+						platformIndex = -1;
+					}
+				}
+				else
+				{
+					GUI.skin.button.imagePosition = ImagePosition.ImageOnly;
+					if (GUILayout.Toggle(false, _platformNames[i], GUI.skin.button))
+					{
+						platformIndex = i;
+					}
+					GUI.skin.button.imagePosition = ImagePosition.ImageLeft;
+				}
+				
+				GUI.backgroundColor = Color.white;
+				GUI.contentColor = Color.white;
+				GUI.color = Color.white;
+				i++;
+			}
+
+			//_platformIndex = GUILayout.SelectionGrid(_platformIndex, _platformNames, 3);
+			//return;
+#if false
+			int rowCount = 0;
+			int platformIndex = _platformIndex;
+			const int itemsPerLine = 4;
+			for (int i = 0; i < _platformNames.Length; i++)
+			{
+				if (i % itemsPerLine == 0)
+				{
+					GUILayout.BeginHorizontal();
+					rowCount++;
+				}
+				MediaPlayer.PlatformOptions options = media.GetPlatformOptions((Platform)i);
+
+				Color hilight = Color.yellow;
+
+				if (i == _platformIndex)
+				{
+					// Selected, unmodified
+					if (!options.IsModified())
+					{
+						GUI.contentColor = Color.white;
+					}
+					else
+					{
+						// Selected, modified
+						GUI.color = hilight;
+						GUI.contentColor = Color.white;
+					}
+				}
+				else if (options.IsModified())
+				{
+					// Unselected, modified
+					GUI.backgroundColor = Color.grey* hilight;
+					GUI.contentColor = hilight;
+				}
+				else
+				{
+					// Unselected, unmodified
+					if (EditorGUIUtility.isProSkin)
+					{
+						GUI.backgroundColor = Color.grey;
+						GUI.color = new Color(0.65f, 0.66f, 0.65f);// Color.grey;
+					}
+				}
+
+				if (i == _platformIndex)
+				{
+					if (!GUILayout.Toggle(true, _platformNames[i], GUI.skin.button))
+					{
+						platformIndex = -1;
+					}
+				}
+				else
+				{
+					GUI.skin.button.imagePosition = ImagePosition.ImageOnly;
+					if (GUILayout.Toggle(false, _platformNames[i], GUI.skin.button))
+					{
+						platformIndex = i;
+					}
+					GUI.skin.button.imagePosition = ImagePosition.ImageLeft;
+				}
+				if ((i+1) % itemsPerLine == 0)
+				{
+					rowCount--;
+					GUILayout.EndHorizontal();
+				}
+				GUI.backgroundColor = Color.white;
+				GUI.contentColor = Color.white;
+				GUI.color = Color.white;
+			}
+
+			if (rowCount > 0)
+			{
+				GUILayout.EndHorizontal();
+			}
+#endif
+			//platformIndex = GUILayout.SelectionGrid(_platformIndex, Helper.GetPlatformNames(), 3);
+			//int platformIndex = GUILayout.Toolbar(_platformIndex, Helper.GetPlatformNames());
+
+			if (platformIndex != _platformIndex)
+			{
+				_platformIndex = platformIndex;
+
+				// We do this to clear the focus, otherwise a focused text field will not change when the Toolbar index changes
+				EditorGUI.FocusTextInControl("ClearFocus");
+			}
+		}
+
+		private void OnInspectorGUI_PlatformOverrides()
+		{
+			foreach (AnimCollapseSection section in _platformSections)
+			{
+				AnimCollapseSection.Show(section, indentLevel:2);
+			}
+		}
+
+		private readonly static GUIContent[] _audio360ChannelMapGuiNames =
+		{
+			new GUIContent("(TBE_8_2) 8 channels of hybrid TBE ambisonics and 2 channels of head-locked stereo audio"),
+			new GUIContent("(TBE_8) 8 channels of hybrid TBE ambisonics. NO head-locked stereo audio"),
+			new GUIContent("(TBE_6_2) 6 channels of hybrid TBE ambisonics and 2 channels of head-locked stereo audio"),
+			new GUIContent("(TBE_6) 6 channels of hybrid TBE ambisonics. NO head-locked stereo audio"),
+			new GUIContent("(TBE_4_2) 4 channels of hybrid TBE ambisonics and 2 channels of head-locked stereo audio"),
+			new GUIContent("(TBE_4) 4 channels of hybrid TBE ambisonics. NO head-locked stereo audio"),
+
+			new GUIContent("(TBE_8_PAIR0) Channels 1 and 2 of TBE hybrid ambisonics"),
+			new GUIContent("(TBE_8_PAIR1) Channels 3 and 4 of TBE hybrid ambisonics"),
+			new GUIContent("(TBE_8_PAIR2) Channels 5 and 6 of TBE hybrid ambisonics"),
+			new GUIContent("(TBE_8_PAIR3) Channels 7 and 8 of TBE hybrid ambisonics"),
+
+			new GUIContent("(TBE_CHANNEL0) Channels 1 of TBE hybrid ambisonics"),
+			new GUIContent("(TBE_CHANNEL1) Channels 2 of TBE hybrid ambisonics"),
+			new GUIContent("(TBE_CHANNEL2) Channels 3 of TBE hybrid ambisonics"),
+			new GUIContent("(TBE_CHANNEL3) Channels 4 of TBE hybrid ambisonics"),
+			new GUIContent("(TBE_CHANNEL4) Channels 5 of TBE hybrid ambisonics"),
+			new GUIContent("(TBE_CHANNEL5) Channels 6 of TBE hybrid ambisonics"),
+			new GUIContent("(TBE_CHANNEL6) Channels 7 of TBE hybrid ambisonics"),
+			new GUIContent("(TBE_CHANNEL7) Channels 8 of TBE hybrid ambisonics"),
+
+			new GUIContent("(HEADLOCKED_STEREO) Head-locked stereo audio"),
+			new GUIContent("(HEADLOCKED_CHANNEL0) Channels 1 or left of head-locked stereo audio"),
+			new GUIContent("(HEADLOCKED_CHANNEL1) Channels 2 or right of head-locked stereo audio"),
+
+			new GUIContent("(AMBIX_4) 4 channels of first order ambiX"),
+			new GUIContent("(AMBIX_4_2) 4 channels of first order ambiX with 2 channels of head-locked audio"),
+			new GUIContent("(AMBIX_9) 9 channels of second order ambiX"),
+			new GUIContent("(AMBIX_9_2) 9 channels of second order ambiX with 2 channels of head-locked audio"),
+			new GUIContent("(AMBIX_16) 16 channels of third order ambiX"),
+			new GUIContent("(AMBIX_16_2) 16 channels of third order ambiX with 2 channels of head-locked audio"),
+
+			new GUIContent("(MONO) Mono audio"),
+			new GUIContent("(STEREO) Stereo audio"),
+		};
+
+		private struct FieldDescription
+		{
+			public FieldDescription(string fieldName, GUIContent description)
+			{
+				this.fieldName = fieldName;
+				this.description = description;
+			}
+			public string fieldName;
+			public GUIContent description;
+		}
+
+		private SerializedProperty DisplayPlatformOption(string platformOptionsFieldName, FieldDescription option)
+		{
+			return DisplayPlatformOption(this.serializedObject, platformOptionsFieldName + option.fieldName, option.description);
+		}
+
+		private static SerializedProperty DisplayPlatformOption(SerializedObject so, string fieldName, GUIContent description)
+		{
+			SerializedProperty prop = so.FindProperty(fieldName);
+			if (prop != null)
+			{
+				if (description == GUIContent.none)
+				{
+					EditorGUILayout.PropertyField(prop, true);
+				}
+				else
+				{
+					EditorGUILayout.PropertyField(prop, description, true);
+				}
+			}
+			else
+			{
+				Debug.LogWarning("Can't find property `" + fieldName + "`");
+			}
+			return prop;
+		}
+
+		private SerializedProperty DisplayPlatformOptionEnum(string platformOptionsFieldName, FieldDescription option, GUIContent[] enumNames)
+		{
+			return DisplayPlatformOptionEnum(this.serializedObject, platformOptionsFieldName + option.fieldName, option.description, enumNames);
+		}
+
+		private static SerializedProperty DisplayPlatformOptionEnum(SerializedObject so, string fieldName, GUIContent description, GUIContent[] enumNames)
+		{
+			SerializedProperty prop = so.FindProperty(fieldName);
+			if (prop != null)
+			{
+				prop.enumValueIndex = EditorGUILayout.Popup(description, prop.enumValueIndex, enumNames);
+			}
+			else
+			{
+				Debug.LogWarning("Can't find property `" + fieldName + "`");
+			}
+			return prop;
+		}
+
+#if false
+		private void OnInspectorGUI_HlsDecryption(string optionsVarName)
+		{
+			EditorGUILayout.BeginVertical(GUI.skin.box);
+			GUILayout.Label("HLS Decryption", EditorStyles.boldLabel);
+
+			// Key server auth token
+			SerializedProperty prop = serializedObject.FindProperty(optionsVarName + ".keyAuth.keyServerToken");
+			if (prop != null)
+			{
+				EditorGUILayout.PropertyField(prop, new GUIContent("Key Server Auth Token", "Token to pass to the key server in the 'Authorization' HTTP header field"));
+			}
+
+			GUILayout.Label("Overrides");
+			EditorGUI.indentLevel++;
+
+			// Key server override
+			/*prop = serializedObject.FindProperty(optionsVarName + ".keyServerURLOverride");
+			if (prop != null)
+			{
+				EditorGUILayout.PropertyField(prop, new GUIContent("Key Server URL", "Overrides the key server URL if present in a HLS manifest."));
+			}*/
+			
+			// Key data blob override
+			prop = serializedObject.FindProperty(optionsVarName + ".keyAuth.overrideDecryptionKeyBase64");
+			if (prop != null)
+			{
+				EditorGUILayout.PropertyField(prop, new GUIContent("Key (Base64)", "Override key to use for decoding encrypted HLS streams (in Base64 format)."));
+			}
+
+			EditorGUI.indentLevel--;
+
+			EditorGUILayout.EndVertical();
+		}
+
+		private void OnInspectorGUI_HttpHeaders(string platformOptionsVarName)
+		{
+			SerializedProperty httpHeadersProp = serializedObject.FindProperty(platformOptionsVarName + ".httpHeaders.httpHeaders");
+			if (httpHeadersProp != null)
+			{
+
+				if (BeginCollapsableSection("Custom HTTP Headers", ref _HTTPHeadersToggle))
+				{
+					{
+						if (httpHeadersProp.arraySize > 0)
+						{
+							int deleteIndex = -1;
+							for (int i = 0; i < httpHeadersProp.arraySize; ++i)
+							{
+								SerializedProperty httpHeaderProp = httpHeadersProp.GetArrayElementAtIndex(i);
+								SerializedProperty headerProp = httpHeaderProp.FindPropertyRelative("name");
+
+								GUILayout.BeginVertical(GUI.skin.box);
+								GUILayout.BeginHorizontal();
+
+								GUI.color = HttpHeader.IsValid(headerProp.stringValue)?Color.white:Color.red;
+								EditorGUILayout.PropertyField(headerProp, GUIContent.none);
+								headerProp.stringValue = headerProp.stringValue.Trim();
+								GUI.color = Color.white;
+
+								if (GUILayout.Button("-", GUILayout.ExpandWidth(false)))
+								{
+									deleteIndex = i;
+								}
+								GUILayout.EndHorizontal();
+
+								SerializedProperty valueProp = httpHeaderProp.FindPropertyRelative("value");
+								GUI.color = HttpHeader.IsValid(valueProp.stringValue)?Color.white:Color.red;
+								valueProp.stringValue = EditorGUILayout.TextArea(valueProp.stringValue, EditorHelper.IMGUI.GetWordWrappedTextAreaStyle());
+								GUI.color = Color.white;
+								valueProp.stringValue = valueProp.stringValue.Trim();
+								GUILayout.EndVertical();
+								GUILayout.Space(4f);
+							}
+
+							if (deleteIndex >= 0)
+							{
+								httpHeadersProp.DeleteArrayElementAtIndex(deleteIndex);
+							}
+						}
+						if (GUILayout.Button("+"))
+						{
+							httpHeadersProp.InsertArrayElementAtIndex(httpHeadersProp.arraySize);
+						}
+					}
+				}
+				EndCollapsableSection();
+			}
+		}
+#endif
+	}
+}

+ 12 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_Platforms.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 02b6040b5ca06424e8ca01ecad239291
+timeCreated: 1448902492
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 874 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_Player.cs

@@ -0,0 +1,874 @@
+using UnityEngine;
+using UnityEditor;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// Editor for the MediaPlayer component
+	/// </summary>
+	public partial class MediaPlayerEditor : UnityEditor.Editor
+	{
+		private static GUIContent FilePathSplitEllipses = new GUIContent("-");
+		private static GUIContent _iconPlayButton;
+		private static GUIContent _iconPauseButton;
+		private static GUIContent _iconSceneViewAudio;
+		private static GUIContent _iconProject;
+		private static GUIContent _iconRotateTool;
+
+		private static bool _showAlpha = false;
+		private static bool _showPreview = false;
+		private static Material _materialResolve;
+		private static Material _materialIMGUI;
+		private static RenderTexture _previewTexture;
+		private static float _lastTextureRatio = -1f;
+		private static int _previewTextureFrameCount = -1;
+
+		private MediaReference _queuedLoadMediaRef = null;
+		private bool _queuedToggleShowPreview = false;
+
+		private void OnInspectorGUI_MediaInfo()
+		{
+			MediaPlayer media = (this.target) as MediaPlayer;
+			IMediaInfo info = media.Info;
+			IMediaControl control = media.Control;
+			ITextTracks textTracks = media.TextTracks;
+			IAudioTracks audioTracks = media.AudioTracks;
+			IVideoTracks videoTracks = media.VideoTracks;
+			if (info != null)
+			{
+				if (!info.HasVideo() && !info.HasAudio())// && !info.HasText())
+				{
+					GUILayout.Label("No media loaded");
+				}
+				else
+				{
+					if (info.HasVideo())
+					{
+						GUILayout.BeginHorizontal();
+						{
+							string dimensionText = string.Format("{0}x{1}@{2:0.##}", info.GetVideoWidth(), info.GetVideoHeight(), info.GetVideoFrameRate());
+							GUILayout.Label(dimensionText);
+							GUILayout.FlexibleSpace();
+							string rateText = "0.00";
+							if (media.Info != null)
+							{
+								rateText = media.Info.GetVideoDisplayRate().ToString("F2");
+							}
+							GUILayout.Label(rateText + "FPS");
+						}
+						GUILayout.EndHorizontal();
+
+						EditorGUILayout.Space();
+					}
+					if (info.HasVideo())
+					{
+						VideoTracks tracks = videoTracks.GetVideoTracks();
+						if (tracks.Count > 0)
+						{
+							GUILayout.Label("Video Tracks: " + tracks.Count);
+							foreach (VideoTrack track in tracks)
+							{
+								bool isActiveTrack = (track == videoTracks.GetActiveVideoTrack());
+								GUI.color = isActiveTrack?Color.green:Color.white;
+								{
+									if (GUILayout.Button(track.DisplayName))
+									{
+										if (isActiveTrack)
+										{
+											videoTracks.SetActiveVideoTrack(null);
+										}
+										else
+										{
+											videoTracks.SetActiveVideoTrack(track);
+										}
+									}
+								}
+							}
+							GUI.color = Color.white;
+							EditorGUILayout.Space();
+						}
+					}
+					if (info.HasAudio())
+					{
+						AudioTracks tracks = audioTracks.GetAudioTracks();
+						if (tracks.Count > 0)
+						{
+							GUILayout.Label("Audio Tracks: " + tracks.Count);
+							foreach (AudioTrack track in tracks)
+							{
+								bool isActiveTrack = (track == audioTracks.GetActiveAudioTrack());
+								GUI.color = isActiveTrack?Color.green:Color.white;
+								{
+									if (GUILayout.Button(track.DisplayName))
+									{
+										if (isActiveTrack)
+										{
+											audioTracks.SetActiveAudioTrack(null);
+										}
+										else
+										{
+											audioTracks.SetActiveAudioTrack(track);
+										}
+									}
+								}
+							}
+							GUI.color = Color.white;
+
+							/*int channelCount = control.GetAudioChannelCount();
+							if (channelCount > 0)
+							{
+								GUILayout.Label("Audio Channels: " + channelCount);
+								AudioChannelMaskFlags audioChannels = control.GetAudioChannelMask();
+								GUILayout.Label("(" + audioChannels + ")", EditorHelper.IMGUI.GetWordWrappedTextAreaStyle());
+							}*/
+							EditorGUILayout.Space();
+						}
+					}
+					{
+						TextTracks tracks = textTracks.GetTextTracks();
+						if (tracks.Count > 0)
+						{
+							GUILayout.Label("Text Tracks: " + tracks.Count);
+							foreach (TextTrack track in tracks)
+							{
+								bool isActiveTrack = (track == textTracks.GetActiveTextTrack());
+								GUI.color = isActiveTrack?Color.green:Color.white;
+								{
+									if (GUILayout.Button(track.DisplayName))
+									{
+										if (isActiveTrack)
+										{
+											textTracks.SetActiveTextTrack(null);
+										}
+										else
+										{
+											textTracks.SetActiveTextTrack(track);
+										}
+									}
+								}
+							}
+							GUI.color = Color.white;
+
+							if (textTracks.GetActiveTextTrack() != null)
+							{
+								string text = string.Empty;
+								if (textTracks.GetCurrentTextCue() != null)
+								{
+									text = textTracks.GetCurrentTextCue().Text;
+									// Clip the text if it is too long
+									if (text.Length >= 96)
+									{
+										text = string.Format("{0}...({1} chars)", text.Substring(0, 96), text.Length);
+									}
+								}
+								GUILayout.Label(text, EditorHelper.IMGUI.GetWordWrappedTextAreaStyle(), GUILayout.Height(48f));
+							}
+							
+							EditorGUILayout.Space();
+						}
+					}
+				}
+			}
+			else
+			{
+				GUILayout.Label("No media loaded");
+			}
+		}
+
+		private void ClosePreview()
+		{
+			if (_materialResolve)
+			{
+				DestroyImmediate(_materialResolve); _materialResolve = null;
+			}
+			if (_materialIMGUI)
+			{
+				DestroyImmediate(_materialIMGUI); _materialIMGUI = null;
+			}
+			if (_previewTexture)
+			{
+				RenderTexture.ReleaseTemporary(_previewTexture); _previewTexture = null;
+			}
+		}
+
+		private void RenderPreview(MediaPlayer media)
+		{
+			int textureFrameCount = media.TextureProducer.GetTextureFrameCount();
+			if (textureFrameCount != _previewTextureFrameCount)
+			{
+				_previewTextureFrameCount = textureFrameCount;
+
+				if (!_materialResolve)
+				{
+					_materialResolve = VideoRender.CreateResolveMaterial( false );
+					VideoRender.SetupResolveMaterial(_materialResolve, VideoResolveOptions.Create());
+				}
+				if (!_materialIMGUI)
+				{
+					_materialIMGUI = VideoRender.CreateIMGUIMaterial();
+				}
+
+				VideoRender.SetupMaterialForMedia(_materialResolve, media, -1);
+
+				VideoRender.ResolveFlags resolveFlags = (VideoRender.ResolveFlags.ColorspaceSRGB | VideoRender.ResolveFlags.Mipmaps | VideoRender.ResolveFlags.PackedAlpha | VideoRender.ResolveFlags.StereoLeft);
+				_previewTexture = VideoRender.ResolveVideoToRenderTexture(_materialResolve, _previewTexture, media.TextureProducer, resolveFlags);
+			}
+		}
+
+		private void DrawCenterCroppedLabel(Rect rect, string text)
+		{
+			if (Event.current.type != EventType.Repaint) return;
+			GUIContent textContent = new GUIContent(text);
+			Vector2 textSize = GUI.skin.label.CalcSize(textContent);
+			if (textSize.x > rect.width)
+			{
+				float ellipseWidth = GUI.skin.label.CalcSize(FilePathSplitEllipses).x;
+
+				// Left
+				Rect rleft = rect;
+				rleft.xMax -= (rleft.width / 2f);
+				rleft.xMax -= (ellipseWidth / 2f);
+				GUI.Label(rleft, textContent);
+
+				// Right
+				Rect rRight = rect;
+				rRight.xMin += (rRight.width / 2f);
+				rRight.xMin += (ellipseWidth / 2f);
+				GUI.Label(rRight, textContent, EditorHelper.IMGUI.GetRightAlignedLabelStyle());
+
+				// Center
+				Rect rCenter = rect;
+				rCenter.xMin += (rect.width / 2f) - (ellipseWidth / 2f);
+				rCenter.xMax -= (rect.width / 2f) - (ellipseWidth / 2f);
+				GUI.Label(rCenter, FilePathSplitEllipses, EditorHelper.IMGUI.GetCenterAlignedLabelStyle());
+			}
+			else
+			{
+				GUI.Label(rect, textContent, EditorHelper.IMGUI.GetCenterAlignedLabelStyle());
+			}
+		}
+
+		private void OnInspectorGUI_Player(MediaPlayer mediaPlayer, ITextureProducer textureSource)
+		{
+			EditorGUILayout.BeginVertical(GUI.skin.box);
+
+			Rect titleRect = Rect.zero;
+			// Display filename as title of preview
+			{
+				string mediaFileName = string.Empty;
+				if ((MediaSource)_propMediaSource.enumValueIndex == MediaSource.Path)
+				{
+					mediaFileName = mediaPlayer.MediaPath.Path;
+				}
+				else if ((MediaSource)_propMediaSource.enumValueIndex == MediaSource.Reference)
+				{
+					if (_propMediaReference.objectReferenceValue != null)
+					{
+						mediaFileName = ((MediaReference)_propMediaReference.objectReferenceValue).GetCurrentPlatformMediaReference().MediaPath.Path;
+					}
+				}
+
+				// Display the file name, cropping if necessary
+				if (!string.IsNullOrEmpty(mediaFileName) && 
+					(0 > mediaFileName.IndexOfAny(System.IO.Path.GetInvalidPathChars())))
+				{
+					string text = System.IO.Path.GetFileName(mediaFileName);
+					titleRect = GUILayoutUtility.GetRect(GUIContent.none, GUI.skin.label);
+
+					// Draw background
+					GUI.Box(titleRect, GUIContent.none, EditorStyles.toolbarButton);
+					DrawCenterCroppedLabel(titleRect, text);
+				}
+			}
+
+			// Toggle preview
+			if (Event.current.type == EventType.MouseDown && Event.current.button == 0 && Event.current.isMouse)
+			{
+				if (titleRect.Contains(Event.current.mousePosition))
+				{
+					_queuedToggleShowPreview = true;
+				}
+			}
+
+			if (_showPreview)
+			{ 
+				Texture texture = EditorGUIUtility.whiteTexture;
+				float textureRatio = 16f / 9f;
+
+				if (_lastTextureRatio > 0f)
+				{
+					textureRatio = _lastTextureRatio;
+				}
+			
+				if (textureSource != null && textureSource.GetTexture() != null)
+				{
+					texture = textureSource.GetTexture();
+					if (_previewTexture)
+					{
+						texture = _previewTexture;
+					}
+					_lastTextureRatio = textureRatio = (((float)texture.width / (float)texture.height) * textureSource.GetTexturePixelAspectRatio());
+				}
+
+				// Reserve rectangle for texture
+				//GUILayout.BeginHorizontal(GUILayout.MaxHeight(Screen.height / 2f), GUILayout.ExpandHeight(true));
+				//GUILayout.FlexibleSpace();
+				Rect textureRect;
+				//textureRect = GUILayoutUtility.GetRect(256f, 256f);
+				if (texture != EditorGUIUtility.whiteTexture)
+				{
+					if (_showAlpha)
+					{
+						float rectRatio = textureRatio * 2f;
+						rectRatio = Mathf.Max(1f, rectRatio);
+						textureRect = GUILayoutUtility.GetAspectRect(rectRatio, GUILayout.ExpandWidth(true));
+					}
+					else
+					{
+						//textureRatio *= 2f;
+						float rectRatio = Mathf.Max(1f, textureRatio);
+						textureRect = GUILayoutUtility.GetAspectRect(rectRatio, GUILayout.ExpandWidth(true), GUILayout.Height(256f));
+						/*GUIStyle style = new GUIStyle(GUI.skin.box);
+						style.stretchHeight = true;
+						style.stretchWidth = true;
+						style.fixedWidth = 0;
+						style.fixedHeight = 0;
+						textureRect = GUILayoutUtility.GetRect(Screen.width, Screen.width, 128f, Screen.height / 1.2f, style);*/
+					}
+				}
+				else
+				{
+					float rectRatio = Mathf.Max(1f, textureRatio);
+					textureRect = GUILayoutUtility.GetAspectRect(rectRatio, GUILayout.ExpandWidth(true), GUILayout.Height(256f));
+				}
+				if (textureRect.height > (Screen.height / 2f))
+				{
+					//textureRect.height = Screen.height / 2f;
+				}
+				//Debug.Log(textureRect.height + " " + Screen.height);
+				//GUILayout.FlexibleSpace();
+				//GUILayout.EndHorizontal();
+
+				// Pause / Play toggle on mouse click
+				if (Event.current.type == EventType.MouseDown && Event.current.button == 0 && Event.current.isMouse)
+				{
+					if (textureRect.Contains(Event.current.mousePosition))
+					{
+						if (mediaPlayer.Control != null)
+						{
+							if (mediaPlayer.Control.IsPaused())
+							{
+								mediaPlayer.Play();
+							}
+							else
+							{
+								mediaPlayer.Pause();
+							}
+						}
+					}
+				}
+
+				if (Event.current.type == EventType.Repaint)
+				{
+					GUI.color = Color.gray;
+					EditorGUI.DrawTextureTransparent(textureRect, Texture2D.blackTexture, ScaleMode.StretchToFill);
+					GUI.color = Color.white;
+					//EditorGUI.DrawTextureAlpha(textureRect, Texture2D.whiteTexture, ScaleMode.ScaleToFit);
+					//GUI.color = Color.black;
+					//GUI.DrawTexture(textureRect, texture, ScaleMode.StretchToFill, false);
+					//GUI.color = Color.white;
+
+					// Draw the texture
+					if (textureSource != null && textureSource.RequiresVerticalFlip())
+					{
+						//	GUIUtility.ScaleAroundPivot(new Vector2(1f, -1f), new Vector2(0f, textureRect.y + (textureRect.height / 2f)));
+					}
+
+					if (!GUI.enabled)
+					{
+						//GUI.color = Color.black;
+						//GUI.DrawTexture(textureRect, texture, ScaleMode.ScaleToFit, false);
+						//GUI.color = Color.white;
+					}
+					else
+					{
+						if (_showPreview && texture != EditorGUIUtility.whiteTexture)
+						{
+							RenderPreview(mediaPlayer);
+						}
+
+						if (!_showAlpha)
+						{
+							if (texture != EditorGUIUtility.whiteTexture)
+							{
+								// TODO: In Linear mode, this displays the texture too bright, but GUI.DrawTexture displays it correctly
+								//GL.sRGBWrite = true;
+								//GUI.DrawTexture(textureRect, rt, ScaleMode.ScaleToFit, false);
+
+								if (_previewTexture)
+								{
+									EditorGUI.DrawPreviewTexture(textureRect, _previewTexture, _materialIMGUI, ScaleMode.ScaleToFit, textureRatio);
+								}
+								//EditorGUI.DrawTextureTransparent(textureRect, rt, ScaleMode.ScaleToFit);
+
+								//VideoRender.DrawTexture(textureRect, rt, ScaleMode.ScaleToFit, AlphaPacking.None, _materialPreview);
+								//GL.sRGBWrite = false;
+							}
+							else
+							{
+								// Fill with black
+								//GUI.color = Color.black;
+								//GUI.DrawTexture(textureRect, texture, ScaleMode.StretchToFill, false);
+								//GUI.color = Color.white;
+							}
+						}
+						else
+						{
+							textureRect.width /= 2f;
+							//GUI.DrawTexture(textureRect, rt, ScaleMode.ScaleToFit, false);
+							//GL.sRGBWrite = true;
+							//VideoRender.DrawTexture(textureRect, rt, ScaleMode.ScaleToFit, AlphaPacking.None, _materialIMGUI);
+							//GL.sRGBWrite = false;
+							textureRect.x += textureRect.width;
+							//EditorGUI.DrawTextureAlpha(textureRect, texture, ScaleMode.ScaleToFit);
+						}
+					}
+				}
+			}
+
+			IMediaInfo info = mediaPlayer.Info;
+			IMediaControl control = mediaPlayer.Control;
+			bool showBrowseMenu = false;
+
+			if (true)
+			{
+				bool isPlaying = false;
+				if (control != null)
+				{
+					isPlaying = control.IsPlaying();
+				}
+
+				// Slider layout
+				EditorGUILayout.BeginHorizontal(GUILayout.Height(EditorGUIUtility.singleLineHeight/2f));
+				Rect sliderRect = GUILayoutUtility.GetRect(GUIContent.none, GUI.skin.horizontalSlider, GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true));
+				EditorGUILayout.EndHorizontal();
+
+				float currentTime = 0f;
+				float durationTime = 0.001f;
+				if (control != null)
+				{
+					currentTime = (float)control.GetCurrentTime();
+					durationTime = (float)info.GetDuration();
+					if (float.IsNaN(durationTime))
+					{
+						durationTime = 0f;
+					}
+				}
+				
+				TimeRange timelineRange = new TimeRange(0.0, 0.001);	// A tiny default duration to prevent divide by zero's
+				if (info != null)
+				{
+					timelineRange = Helper.GetTimelineRange(info.GetDuration(), control.GetSeekableTimes());
+				}
+
+				// Slider
+				{
+					// Draw buffering
+					if (control != null && timelineRange.Duration > 0.0 && Event.current.type == EventType.Repaint)
+					{
+						GUI.color = new Color(0f, 1f, 0f, 0.25f);
+						TimeRanges times = control.GetBufferedTimes();
+						if (timelineRange.Duration > 0.0)
+						{
+							for (int i = 0; i < times.Count; i++)
+							{
+								Rect bufferedRect = sliderRect;
+
+								float startT = Mathf.Clamp01((float)((times[i].StartTime - timelineRange.StartTime) / timelineRange.Duration));
+								float endT = Mathf.Clamp01((float)((times[i].EndTime - timelineRange.StartTime) / timelineRange.Duration));
+
+								bufferedRect.xMin = sliderRect.xMin + sliderRect.width * startT;
+								bufferedRect.xMax = sliderRect.xMin + sliderRect.width * endT;
+								bufferedRect.yMin += sliderRect.height * 0.5f;
+								
+								GUI.DrawTexture(bufferedRect, Texture2D.whiteTexture);
+							}
+						}
+						GUI.color = Color.white;
+					}
+
+					// Timeline slider
+					{
+						float newTime = GUI.HorizontalSlider(sliderRect, currentTime, (float)timelineRange.StartTime, (float)timelineRange.EndTime);
+						if (newTime != currentTime)
+						{
+							if (control != null)
+							{
+								// NOTE: For unknown reasons the seeks here behave differently to the MediaPlayerUI demo
+								// When scrubbing (especially with NotchLC) while the video is playing, the frames will not update and a Stalled state will be shown,
+								// but using the MediaPlayerUI the same scrubbing will updates the frames.  Perhaps it's just an execution order issue
+								control.Seek(newTime);
+							}
+						}
+					}
+				}
+
+				EditorGUILayout.BeginHorizontal();
+				string timeTotal = "∞";
+				if (!float.IsInfinity(durationTime))
+				{
+					timeTotal = Helper.GetTimeString(durationTime, false);
+				}
+				string timeUsed = Helper.GetTimeString(currentTime - (float)timelineRange.StartTime, false);
+				GUILayout.Label(timeUsed, GUILayout.ExpandWidth(false));
+				//GUILayout.Label("/", GUILayout.ExpandWidth(false));
+				GUILayout.FlexibleSpace();
+				GUILayout.Label(timeTotal, GUILayout.ExpandWidth(false));
+
+				EditorGUILayout.EndHorizontal();
+
+				// In non-pro we need to make these 3 icon content black as the buttons are light
+				// and the icons are white by default
+				if (!EditorGUIUtility.isProSkin)
+				{
+					GUI.contentColor = Color.black;
+				}
+
+				EditorGUILayout.BeginHorizontal(GUILayout.ExpandWidth(true));
+
+				// Play/Pause
+				{
+					float maxHeight = GUI.skin.button.CalcHeight(_iconSceneViewAudio, 0f);
+					if (!isPlaying)
+					{
+						GUI.color = Color.green;
+						if (GUILayout.Button(_iconPlayButton, GUILayout.ExpandWidth(false), GUILayout.Height(maxHeight)))
+						{
+							if (control != null)
+							{
+								control.Play();
+							}
+							else
+							{
+								if (mediaPlayer.MediaSource == MediaSource.Path)
+								{
+									mediaPlayer.OpenMedia(mediaPlayer.MediaPath.PathType, mediaPlayer.MediaPath.Path, true);
+								}
+								else if (mediaPlayer.MediaSource == MediaSource.Reference)
+								{
+									mediaPlayer.OpenMedia(mediaPlayer.MediaReference, true);
+								}
+							}
+						}
+					}
+					else
+					{
+						GUI.color = Color.yellow;
+						if (GUILayout.Button(_iconPauseButton, GUILayout.ExpandWidth(false), GUILayout.Height(maxHeight)))
+						{
+							if (control != null)
+							{
+								control.Pause();
+							}
+						}
+					}
+					GUI.color = Color.white;
+				}
+
+				// Looping
+				{
+					if (!_propLoop.boolValue)
+					{
+						GUI.color = Color.grey;
+					}
+					float maxHeight = GUI.skin.button.CalcHeight(_iconSceneViewAudio, 0f);
+					//GUIContent icon = new GUIContent("∞");
+					if (GUILayout.Button(_iconRotateTool, GUILayout.Height(maxHeight)))
+					{
+						if (control != null)
+						{
+							control.SetLooping(!_propLoop.boolValue);
+						}
+						_propLoop.boolValue = !_propLoop.boolValue;
+					}
+					GUI.color = Color.white;
+				}				
+
+				// Mute & Volume
+				EditorGUI.BeginDisabledGroup(UnityEditor.EditorUtility.audioMasterMute);
+				{
+					if (_propMuted.boolValue)
+					{
+						GUI.color = Color.gray;
+					}
+					float maxWidth = _iconPlayButton.image.width;
+					//if (GUILayout.Button("Muted", GUILayout.ExpandWidth(false), GUILayout.Height(EditorGUIUtility.singleLineHeight)))
+					//string iconName = "d_AudioListener Icon";		// Unity 2019+
+					if (GUILayout.Button(_iconSceneViewAudio))//, GUILayout.Width(maxWidth),  GUILayout.Height(EditorGUIUtility.singleLineHeight), GUILayout.ExpandHeight(false)))
+					{
+						if (control != null)
+						{
+							control.MuteAudio(!_propMuted.boolValue);
+						}
+						_propMuted.boolValue = !_propMuted.boolValue;
+					}
+					GUI.color = Color.white;
+				}
+				if (!_propMuted.boolValue)
+				{
+					EditorGUI.BeginChangeCheck();
+					float newVolume = GUILayout.HorizontalSlider(_propVolume.floatValue, 0f, 1f,  GUILayout.ExpandWidth(true), GUILayout.MinWidth(64f));
+					if (EditorGUI.EndChangeCheck())
+					{
+						if (control != null)
+						{
+							control.SetVolume(newVolume);
+						}
+						_propVolume.floatValue = newVolume;
+					}
+				}
+				EditorGUI.EndDisabledGroup();
+
+				GUI.contentColor = Color.white;
+
+				GUILayout.FlexibleSpace();
+
+				if (Event.current.commandName == "ObjectSelectorClosed" && 
+					EditorGUIUtility.GetObjectPickerControlID() == 200) 
+				{
+					_queuedLoadMediaRef = (MediaReference)EditorGUIUtility.GetObjectPickerObject();
+				}
+
+				if (GUILayout.Button(_iconProject, GUILayout.ExpandWidth(false)))
+				{
+					showBrowseMenu = true;
+				}
+				
+				EditorGUILayout.EndHorizontal();
+			}
+			EditorGUILayout.EndVertical();
+
+			if (showBrowseMenu)
+			{
+				RecentMenu.Create(_propMediaPath, _propMediaSource, MediaFileExtensions, true, 200);
+			}
+
+			if (_queuedLoadMediaRef && Event.current.type == EventType.Repaint)
+			{
+				//MediaPlayer mediaPlayer = (MediaPlayer)_propMediaPath.serializedObject.targetObject;
+				if (mediaPlayer)
+				{
+					mediaPlayer.OpenMedia(_queuedLoadMediaRef, true);
+					_queuedLoadMediaRef = null;
+				}
+			}
+			if (_queuedToggleShowPreview)
+			{
+				_showPreview = !_showPreview;
+				_queuedToggleShowPreview = false;
+				this.Repaint();
+			}
+		}
+
+		private void OnInspectorGUI_VideoPreview(MediaPlayer media, ITextureProducer textureSource)
+		{
+			EditorGUILayout.LabelField("* Inspector preview affects playback performance");
+
+			Texture texture = null;
+			if (textureSource != null)
+			{
+				texture = textureSource.GetTexture();
+			}
+			if (texture == null)
+			{
+				texture = EditorGUIUtility.whiteTexture;
+			}
+
+			float ratio = (float)texture.width / (float)texture.height;
+
+			// Reserve rectangle for texture
+			GUILayout.BeginHorizontal();
+			GUILayout.FlexibleSpace();
+			Rect textureRect;
+			if (texture != EditorGUIUtility.whiteTexture)
+			{
+				if (_showAlpha)
+				{
+					ratio *= 2f;
+					textureRect = GUILayoutUtility.GetRect(Screen.width / 2, Screen.width / 2, (Screen.width / 2) / ratio, (Screen.width / 2) / ratio);
+				}
+				else
+				{
+					textureRect = GUILayoutUtility.GetRect(Screen.width / 2, Screen.width / 2, (Screen.width / 2) / ratio, (Screen.width / 2) / ratio);
+				}
+			}
+			else
+			{
+				textureRect = GUILayoutUtility.GetRect(1920f / 40f, 1080f / 40f, GUILayout.ExpandWidth(true));
+			}
+			GUILayout.FlexibleSpace();
+			GUILayout.EndHorizontal();
+
+			// Dimensions
+			string dimensionText = string.Format("{0}x{1}@{2:0.##}", 0, 0, 0.0f);
+			if (texture != EditorGUIUtility.whiteTexture && media.Info != null)
+			{
+				dimensionText = string.Format("{0}x{1}@{2:0.##}", texture.width, texture.height, media.Info.GetVideoFrameRate());
+			}
+
+			EditorHelper.IMGUI.CentreLabel(dimensionText);
+
+			string rateText = "0";
+			string playerText = string.Empty;
+			if (media.Info != null)
+			{
+				rateText = media.Info.GetVideoDisplayRate().ToString("F2");
+				playerText = media.Info.GetPlayerDescription();
+			}
+
+			EditorGUILayout.LabelField("Display Rate", rateText);
+			EditorGUILayout.LabelField("Using", playerText);
+			_showAlpha = EditorGUILayout.Toggle("Show Alpha", _showAlpha);
+
+			// Draw the texture
+			Matrix4x4 prevMatrix = GUI.matrix;
+			if (textureSource != null && textureSource.RequiresVerticalFlip())
+			{
+				GUIUtility.ScaleAroundPivot(new Vector2(1f, -1f), new Vector2(0, textureRect.y + (textureRect.height / 2)));
+			}
+
+			if (!GUI.enabled)
+			{
+				GUI.color = Color.grey;
+				GUI.DrawTexture(textureRect, texture, ScaleMode.ScaleToFit, false);
+				GUI.color = Color.white;
+			}
+			else
+			{
+				if (!_showAlpha)
+				{
+					// TODO: In Linear mode, this displays the texture too bright, but GUI.DrawTexture displays it correctly
+					EditorGUI.DrawTextureTransparent(textureRect, texture, ScaleMode.ScaleToFit);
+				}
+				else
+				{
+					textureRect.width /= 2f;
+					GUI.DrawTexture(textureRect, texture, ScaleMode.ScaleToFit, false);
+					textureRect.x += textureRect.width;
+					EditorGUI.DrawTextureAlpha(textureRect, texture, ScaleMode.ScaleToFit);
+				}
+			}
+			GUI.matrix = prevMatrix;
+
+			// Select texture button
+			/*if (texture != null && texture != EditorGUIUtility.whiteTexture)
+			{
+				GUILayout.BeginHorizontal(GUILayout.ExpandWidth(true));
+				GUILayout.FlexibleSpace();
+				for (int i = 0; i < textureSource.GetTextureCount(); i++)
+				{
+					Texture textures = textureSource.GetTexture(i);
+					if (GUILayout.Button("Select Texture", GUILayout.ExpandWidth(false)))
+					{
+						Selection.activeObject = textures;
+					}
+				}
+				if (GUILayout.Button("Save PNG", GUILayout.ExpandWidth(true)))
+				{
+					media.SaveFrameToPng();
+				}
+				GUILayout.FlexibleSpace();
+				GUILayout.EndHorizontal();
+			}*/
+		}
+
+		private void OnInspectorGUI_PlayControls(IMediaControl control, IMediaInfo info)
+		{
+			GUILayout.Space(8.0f);
+
+			// Slider
+			EditorGUILayout.BeginHorizontal();
+			bool isPlaying = false;
+			if (control != null)
+			{
+				isPlaying = control.IsPlaying();
+			}
+			float currentTime = 0f;
+			if (control != null)
+			{
+				currentTime = (float)control.GetCurrentTime();
+			}
+
+			float durationTime = 0f;
+			if (info != null)
+			{
+				durationTime = (float)info.GetDuration();
+				if (float.IsNaN(durationTime))
+				{
+					durationTime = 0f;
+				}
+			}
+			string timeUsed = Helper.GetTimeString(currentTime, true);
+			GUILayout.Label(timeUsed, GUILayout.ExpandWidth(false));
+
+			float newTime = GUILayout.HorizontalSlider(currentTime, 0f, durationTime, GUILayout.ExpandWidth(true));
+			if (newTime != currentTime)
+			{
+				control.Seek(newTime);
+			}
+
+			string timeTotal = "Infinity";
+			if (!float.IsInfinity(durationTime))
+			{
+				timeTotal = Helper.GetTimeString(durationTime, true);
+			}
+
+			GUILayout.Label(timeTotal, GUILayout.ExpandWidth(false));
+
+			EditorGUILayout.EndHorizontal();
+
+			// Buttons
+			EditorGUILayout.BeginHorizontal();
+			if (GUILayout.Button("Rewind", GUILayout.ExpandWidth(false)))
+			{
+				control.Rewind();
+			}
+
+			if (!isPlaying)
+			{
+				GUI.color = Color.green;
+				if (GUILayout.Button("Play", GUILayout.ExpandWidth(true)))
+				{
+					control.Play();
+				}
+			}
+			else
+			{
+				GUI.color = Color.yellow;
+				if (GUILayout.Button("Pause", GUILayout.ExpandWidth(true)))
+				{
+					control.Pause();
+				}
+			}
+			GUI.color = Color.white;
+			EditorGUILayout.EndHorizontal();
+		}
+
+		void OnInspectorGUI_Preview()
+		{
+			MediaPlayer media = (this.target) as MediaPlayer;
+
+			EditorGUI.BeginDisabledGroup(!(media.TextureProducer != null && media.Info.HasVideo()));
+			OnInspectorGUI_VideoPreview(media, media.TextureProducer);
+			EditorGUI.EndDisabledGroup();
+
+			EditorGUI.BeginDisabledGroup(!(media.Control != null && media.Control.CanPlay() && media.isActiveAndEnabled && !EditorApplication.isPaused));
+			OnInspectorGUI_PlayControls(media.Control, media.Info);
+			EditorGUI.EndDisabledGroup();
+		}
+	}
+}

+ 12 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_Player.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 09461ea66270ee847aceeb2a5fa2fb81
+timeCreated: 1448902492
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 389 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_Source.cs

@@ -0,0 +1,389 @@
+using UnityEngine;
+using UnityEditor;
+using System.Collections.Generic;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// Editor for the MediaPlayer component
+	/// </summary>
+	public partial class MediaPlayerEditor : UnityEditor.Editor
+	{
+#if UNITY_EDITOR_OSX
+		internal const string MediaFileExtensions = "mp4,m4v,mov,mpg,avi,mp3,m4a,aac,ac3,au,aiff,caf,wav,m3u8";
+#else
+		internal const string MediaFileExtensions = "Media Files;*.mp4;*.mov;*.m4v;*.avi;*.mkv;*.ts;*.webm;*.flv;*.vob;*.ogg;*.ogv;*.mpg;*.wmv;*.3gp;*.mxf;Audio Files;*wav;*.mp3;*.mp2;*.m4a;*.wma;*.aac;*.au;*.flac;*.m3u8;*.mpd;*.ism;";
+#endif
+
+		private readonly static GUIContent[] _fileFormatGuiNames =
+		{
+			new GUIContent("Automatic (by extension)"),
+			new GUIContent("Apple HLS (.m3u8)"),
+			new GUIContent("MPEG-DASH (.mdp)"),
+			new GUIContent("MS Smooth Streaming (.ism)"),
+		};
+
+		private SerializedProperty _propMediaSource;
+		private SerializedProperty _propMediaReference;
+		private SerializedProperty _propMediaPath;
+
+		private void OnInspectorGUI_Source()
+		{
+			// Display the file name and buttons to load new files
+			MediaPlayer mediaPlayer = (this.target) as MediaPlayer;
+
+			EditorGUILayout.PropertyField(_propMediaSource);
+
+			if (MediaSource.Reference == (MediaSource)_propMediaSource.enumValueIndex)
+			{
+				EditorGUILayout.PropertyField(_propMediaReference);
+			}
+
+			EditorGUILayout.BeginVertical(GUI.skin.box);
+
+			if (MediaSource.Reference != (MediaSource)_propMediaSource.enumValueIndex)
+			{
+				OnInspectorGUI_CopyableFilename(mediaPlayer.MediaPath.Path);
+				EditorGUILayout.PropertyField(_propMediaPath);
+			}
+
+			//if (!Application.isPlaying)
+			{
+				GUI.color = Color.white;
+				GUILayout.BeginHorizontal();
+
+				if (_allowDeveloperMode)
+				{
+					if (GUILayout.Button("Rewind"))
+					{
+						mediaPlayer.Rewind(true);
+					}
+					if (GUILayout.Button("Preroll"))
+					{
+						mediaPlayer.RewindPrerollPause();
+					}
+					if (GUILayout.Button("End"))
+					{
+						mediaPlayer.Control.Seek(mediaPlayer.Info.GetDuration());
+					}
+				}
+				if (GUILayout.Button("Close"))
+				{
+					mediaPlayer.CloseMedia();
+				}
+				if (GUILayout.Button("Load"))
+				{
+					if (mediaPlayer.MediaSource == MediaSource.Path)
+					{
+						mediaPlayer.OpenMedia(mediaPlayer.MediaPath.PathType, mediaPlayer.MediaPath.Path, mediaPlayer.AutoStart);
+					}
+					else if (mediaPlayer.MediaSource == MediaSource.Reference)
+					{
+						mediaPlayer.OpenMedia(mediaPlayer.MediaReference, mediaPlayer.AutoStart);
+					}
+				}
+				/*if (media.Control != null)
+				{
+					if (GUILayout.Button("Unload"))
+					{
+						media.CloseVideo();
+					}
+				}*/
+
+				if (EditorGUIUtility.GetObjectPickerControlID() == 100 &&
+					Event.current.commandName == "ObjectSelectorClosed")
+				{
+					MediaReference mediaRef = (MediaReference)EditorGUIUtility.GetObjectPickerObject();
+					if (mediaRef)
+					{
+						_propMediaSource.enumValueIndex = (int)MediaSource.Reference;
+						_propMediaReference.objectReferenceValue = mediaRef;
+					}
+				}
+
+				GUI.color = Color.green;
+				MediaPathDrawer.ShowBrowseButtonIcon(_propMediaPath, _propMediaSource);
+				GUI.color = Color.white;
+
+				GUILayout.EndHorizontal();
+
+				//MediaPath mediaPath = new MediaPath(_propMediaPath.FindPropertyRelative("_path").stringValue, (MediaPathType)_propMediaPath.FindPropertyRelative("_pathType").enumValueIndex);
+				//ShowFileWarningMessages((MediaSource)_propMediaSource.enumValueIndex, mediaPath, (MediaReference)_propMediaReference.objectReferenceValue, mediaPlayer.AutoOpen, Platform.Unknown);
+				GUI.color = Color.white;
+			}
+
+			if (MediaSource.Reference != (MediaSource)_propMediaSource.enumValueIndex)
+			{
+				GUILayout.Label("Fallback Media Hints", EditorStyles.boldLabel);
+				EditorGUILayout.PropertyField(_propFallbackMediaHints);
+			}
+
+			EditorGUILayout.EndVertical();
+		}
+
+		internal static void OnInspectorGUI_CopyableFilename(string path)
+		{
+			if (EditorGUIUtility.isProSkin)
+			{
+				GUI.backgroundColor = Color.black;
+				GUI.color = Color.cyan;
+			}
+
+			EditorHelper.IMGUI.CopyableFilename(path);
+
+			GUI.color = Color.white;
+			GUI.backgroundColor = Color.white;
+		}
+
+		internal static void ShowFileWarningMessages(MediaSource mediaSource, MediaPath mediaPath, MediaReference mediaReference, bool isAutoOpen, Platform platform)
+		{
+			MediaPath result = null;
+
+			if (mediaSource == MediaSource.Path)
+			{
+				if (mediaPath != null)
+				{
+					result = mediaPath;
+				}
+			}
+			else if (mediaSource == MediaSource.Reference)
+			{
+				if (mediaReference != null)
+				{
+					result = mediaReference.GetCurrentPlatformMediaReference().MediaPath;
+				}
+			}
+
+			ShowFileWarningMessages(result, isAutoOpen, platform);
+		}
+
+		internal static void ShowFileWarningMessages(string filePath, MediaPathType fileLocation, MediaReference mediaReference, MediaSource mediaSource, bool isAutoOpen, Platform platform)
+		{
+			MediaPath mediaPath = null;
+
+			if (mediaSource == MediaSource.Path)
+			{
+				mediaPath = new MediaPath(filePath, fileLocation);
+			}
+			else if (mediaSource == MediaSource.Reference)
+			{
+				if (mediaReference != null)
+				{
+					mediaPath = mediaReference.GetCurrentPlatformMediaReference().MediaPath;
+				}
+			}
+
+			ShowFileWarningMessages(mediaPath, isAutoOpen, platform);
+		}
+
+		internal static void ShowFileWarningMessages(MediaPath mediaPath, bool isAutoOpen, Platform platform)
+		{
+			string fullPath = string.Empty;
+			if (mediaPath != null)
+			{
+				fullPath = mediaPath.GetResolvedFullPath();
+			}
+			if (string.IsNullOrEmpty(fullPath))
+			{
+				if (isAutoOpen)
+				{
+					EditorHelper.IMGUI.NoticeBox(MessageType.Error, "No media specified");
+				}
+				else
+				{
+					EditorHelper.IMGUI.NoticeBox(MessageType.Warning, "No media specified");
+				}
+			}
+			else
+			{
+				bool isPlatformAndroid = (platform == Platform.Android) || (platform == Platform.Unknown && BuildTargetGroup.Android == UnityEditor.EditorUserBuildSettings.selectedBuildTargetGroup);
+				bool isPlatformIOS = (platform == Platform.iOS);
+				isPlatformIOS |= (platform == Platform.Unknown && BuildTargetGroup.iOS == UnityEditor.EditorUserBuildSettings.selectedBuildTargetGroup);
+				bool isPlatformTVOS = (platform == Platform.tvOS);
+
+				isPlatformTVOS |= (platform == Platform.Unknown && BuildTargetGroup.tvOS == UnityEditor.EditorUserBuildSettings.selectedBuildTargetGroup);
+
+				// Test file extensions
+				{
+					bool isExtensionAVI = fullPath.ToLower().EndsWith(".avi");
+					bool isExtensionMOV = fullPath.ToLower().EndsWith(".mov");
+					bool isExtensionMKV = fullPath.ToLower().EndsWith(".mkv");
+
+					if (isPlatformAndroid && isExtensionMOV)
+					{
+						EditorHelper.IMGUI.NoticeBox(MessageType.Warning, "MOV file detected. Android doesn't support MOV files, you should change the container file.");
+					}
+					if (isPlatformAndroid && isExtensionAVI)
+					{
+						EditorHelper.IMGUI.NoticeBox(MessageType.Warning, "AVI file detected. Android doesn't support AVI files, you should change the container file.");
+					}
+					if (isPlatformAndroid && isExtensionMKV)
+					{
+						EditorHelper.IMGUI.NoticeBox(MessageType.Warning, "MKV file detected. Android doesn't support MKV files until Android 5.0.");
+					}
+					if (isPlatformIOS && isExtensionAVI)
+					{
+						EditorHelper.IMGUI.NoticeBox(MessageType.Warning, "AVI file detected. iOS doesn't support AVI files, you should change the container file.");
+					}
+				}
+
+				if (fullPath.Contains("://"))
+				{
+					if (fullPath.ToLower().Contains("rtmp://"))
+					{
+						EditorHelper.IMGUI.NoticeBox(MessageType.Warning, "RTMP protocol is not supported by AVPro Video, except when Windows DirectShow is used with an external codec library (eg LAV Filters)");
+					}
+					if (fullPath.ToLower().Contains("youtube.com/watch"))
+					{
+						EditorHelper.IMGUI.NoticeBox(MessageType.Warning, "YouTube URL detected. YouTube website URL contains no media, a direct media file URL (eg MP4 or M3U8) is required.  See the documentation FAQ for YouTube support.");
+					}
+					if (mediaPath.PathType != MediaPathType.AbsolutePathOrURL)
+					{
+						EditorHelper.IMGUI.NoticeBox(MessageType.Warning, "URL detected, change location type to URL?");
+					}
+					else
+					{
+						// Display warning to iOS users if they're trying to use HTTP url without setting the permission
+						if (isPlatformIOS || isPlatformTVOS)
+						{
+							if (!PlayerSettings.iOS.allowHTTPDownload && fullPath.StartsWith("http://"))
+							{
+								EditorHelper.IMGUI.NoticeBox(MessageType.Warning, "Starting with iOS 9 'allow HTTP downloads' must be enabled for HTTP connections (see Player Settings)");
+							}
+						}
+#if UNITY_ANDROID
+						if (fullPath.StartsWith("http://"))
+						{
+							EditorHelper.IMGUI.NoticeBox(MessageType.Warning, "Starting with Android 8 unsecure HTTP is not allowed by default and HTTPS must be used, unless a custom cleartext security policy is assigned");
+						}
+#endif
+						// Display warning for Android users if they're trying to use a URL without setting permission
+						if (isPlatformAndroid && !PlayerSettings.Android.forceInternetPermission)
+						{
+							EditorHelper.IMGUI.NoticeBox(MessageType.Warning, "You need to set 'Internet Access' to 'require' in your Player Settings for Android builds when using URLs");
+						}
+
+						// Display warning for UWP users if they're trying to use a URL without setting permission
+						if (platform == Platform.WindowsUWP || (platform == Platform.Unknown && (
+							BuildTargetGroup.WSA == UnityEditor.EditorUserBuildSettings.selectedBuildTargetGroup
+							)))
+						{
+							if (!PlayerSettings.WSA.GetCapability(PlayerSettings.WSACapability.InternetClient))
+							{
+								EditorHelper.IMGUI.NoticeBox(MessageType.Warning, "You need to set 'InternetClient' capability in your Player Settings when using URLs");
+							}
+						}
+					}
+				}
+				else
+				{
+					// [MOZ] All paths on (i|mac|tv)OS are absolute so this check just results in an incorrect warning being shown
+					#if !UNITY_EDITOR_OSX
+					if (mediaPath.PathType != MediaPathType.AbsolutePathOrURL && fullPath.StartsWith("/"))
+					{
+						EditorHelper.IMGUI.NoticeBox(MessageType.Warning, "Absolute path detected, change location to Absolute path?");
+					}
+					#endif
+
+					// Display warning for Android users if they're trying to use absolute file path without permission
+					if (isPlatformAndroid && !PlayerSettings.Android.forceSDCardPermission)
+					{
+						EditorHelper.IMGUI.NoticeBox(MessageType.Warning, "You may need to access the local file system you may need to set 'Write Access' to 'External(SDCard)' in your Player Settings for Android");
+					}
+
+					if (platform == Platform.Unknown || platform == MediaPlayer.GetPlatform())
+					{
+						if (!System.IO.File.Exists(fullPath))
+						{
+							EditorHelper.IMGUI.NoticeBox(MessageType.Error, "File not found");
+						}
+						else
+						{
+							// Check the case
+							// This approach is very slow, so we only run it when the app isn't playing
+							if (!Application.isPlaying)
+							{
+								string comparePath = fullPath.Replace('\\', '/');
+								string folderPath = System.IO.Path.GetDirectoryName(comparePath);
+								if (!string.IsNullOrEmpty(folderPath))
+								{
+
+									string[] files = System.IO.Directory.GetFiles(folderPath, "*", System.IO.SearchOption.TopDirectoryOnly);
+									bool caseMatch = false;
+									if (files != null && files.Length > 0)
+									{
+										for (int i = 0; i < files.Length; i++)
+										{
+											if (files[i].Replace('\\', '/') == comparePath)
+											{
+												caseMatch = true;
+												break;
+											}
+										}
+									}
+									if (!caseMatch)
+									{
+										EditorHelper.IMGUI.NoticeBox(MessageType.Warning, "File found but case doesn't match");
+									}
+								}
+							}
+						}
+					}
+				}
+			}
+
+			if (mediaPath != null && mediaPath.PathType == MediaPathType.RelativeToStreamingAssetsFolder)
+			{
+				if (!System.IO.Directory.Exists(Application.streamingAssetsPath))
+				{
+					GUILayout.BeginHorizontal();
+					GUI.color = Color.yellow;
+					GUILayout.TextArea("Warning: No StreamingAssets folder found");
+
+					if (GUILayout.Button("Create Folder"))
+					{
+						System.IO.Directory.CreateDirectory(Application.streamingAssetsPath);
+						AssetDatabase.Refresh();
+					}
+					GUILayout.EndHorizontal();
+				}
+				else
+				{
+					bool checkAndroidFileSize = false;
+#if UNITY_ANDROID
+					if (platform == Platform.Unknown)
+					{
+						checkAndroidFileSize = true;
+					}
+#endif
+					if (platform == Platform.Android)
+					{
+						checkAndroidFileSize = true;
+					}
+
+					if (checkAndroidFileSize)
+					{
+						try
+						{
+							System.IO.FileInfo info = new System.IO.FileInfo(fullPath);
+							if (info != null && info.Length > (1024 * 1024 * 512))
+							{
+								EditorHelper.IMGUI.NoticeBox(MessageType.Warning, "Using this very large file inside StreamingAssets folder on Android isn't recommended.  Deployments will be slow and mapping the file from the StreamingAssets JAR may cause storage and memory issues.  We recommend loading from another folder on the device.");
+							}
+						}
+						catch (System.Exception)
+						{
+						}
+					}
+				}
+			}
+
+			GUI.color = Color.white;
+		}
+
+	}
+}

+ 12 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_Source.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: d48dbd69cf9694e439fa465103879d35
+timeCreated: 1448902492
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 77 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_Subtitles.cs

@@ -0,0 +1,77 @@
+using UnityEngine;
+using UnityEditor;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// Editor for the MediaPlayer component
+	/// </summary>
+	public partial class MediaPlayerEditor : UnityEditor.Editor
+	{
+#if UNITY_EDITOR_OSX
+		internal const string SubtitleFileExtensions = "srt";
+#else
+		internal const string SubtitleFileExtensions = "Subtitle Files;*.srt";
+#endif
+
+		private SerializedProperty _propSubtitles;
+		private SerializedProperty _propSubtitlePath;
+
+		private void OnInspectorGUI_Subtitles()
+		{
+			// TODO: add support for multiple targets?
+			MediaPlayer media = (this.target) as MediaPlayer;
+
+			//EditorGUILayout.BeginVertical();
+			EditorGUILayout.PropertyField(_propSubtitles, new GUIContent("Sideload Subtitles"));
+		
+			EditorGUI.BeginDisabledGroup(!_propSubtitles.boolValue);
+
+			EditorGUILayout.BeginVertical(GUI.skin.box);
+
+			EditorGUILayout.PropertyField(_propSubtitlePath);
+
+			//if (!Application.isPlaying)
+			{
+				GUI.color = Color.white;
+				GUILayout.BeginHorizontal();
+
+				if (Application.isPlaying)
+				{
+					if (GUILayout.Button("Load"))
+					{
+						MediaPath mediaPath = new MediaPath(_propSubtitlePath.FindPropertyRelative("_path").stringValue, (MediaPathType)_propSubtitlePath.FindPropertyRelative("_pathType").enumValueIndex);
+						media.EnableSubtitles(mediaPath);
+					}
+					if (GUILayout.Button("Clear"))
+					{
+						media.DisableSubtitles();
+					}
+				}
+				else
+				{
+					GUILayout.FlexibleSpace();
+				}
+
+				MediaPathDrawer.ShowBrowseSubtitlesButtonIcon(_propSubtitlePath);
+
+				GUILayout.EndHorizontal();
+				if (_propSubtitles.boolValue)
+				{
+					///MediaPath mediaPath = new MediaPath(_propSubtitlePath.FindPropertyRelative("_path").stringValue, (MediaPathType)_propSubtitlePath.FindPropertyRelative("_pathType").enumValueIndex);
+					//ShowFileWarningMessages(mediaPath, media.AutoOpen, Platform.Unknown);
+					//GUI.color = Color.white;
+				}
+			}
+
+			//EditorGUILayout.EndVertical();
+
+			EditorGUILayout.EndVertical();
+			EditorGUI.EndDisabledGroup();
+		}
+	}
+}

+ 12 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_Subtitles.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: a04b089da8164054e9997d11d2b2f9a4
+timeCreated: 1448902492
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 36 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_WebGL.cs

@@ -0,0 +1,36 @@
+using UnityEngine;
+using UnityEditor;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// Editor for the MediaPlayer component
+	/// </summary>
+	public partial class MediaPlayerEditor : UnityEditor.Editor
+	{
+		private readonly static FieldDescription _optionExternalLibrary = new FieldDescription(".externalLibrary", GUIContent.none);
+
+		private void OnInspectorGUI_Override_WebGL()
+		{
+			GUILayout.Space(8f);
+
+			string optionsVarName = MediaPlayer.GetPlatformOptionsVariable(Platform.WebGL);
+
+			EditorGUILayout.BeginVertical(GUI.skin.box);
+
+			DisplayPlatformOption(optionsVarName, _optionExternalLibrary);
+
+			SerializedProperty propUseTextureMips = DisplayPlatformOption(optionsVarName, _optionTextureMips);
+			if (propUseTextureMips.boolValue && ((FilterMode)_propFilter.enumValueIndex) != FilterMode.Trilinear)
+			{
+				EditorHelper.IMGUI.NoticeBox(MessageType.Info, "Recommend changing the texture filtering mode to Trilinear when using mip-maps.");
+			}
+
+			EditorGUILayout.EndVertical();
+		}
+	}
+}

+ 12 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_WebGL.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 204fbdf92f39c6847ac3e270ca56dc09
+timeCreated: 1448902492
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 319 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_Windows.cs

@@ -0,0 +1,319 @@
+using UnityEngine;
+using UnityEditor;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// Editor for the MediaPlayer component
+	/// </summary>
+	public partial class MediaPlayerEditor : UnityEditor.Editor
+	{
+		private SerializedProperty _propSourceAudioSampleRate;
+		private SerializedProperty _propSourceAudioChannels;
+		private SerializedProperty _propManualSetAudioProps;
+
+		private readonly static GUIContent[] _audioModesWindows =
+		{
+			new GUIContent("System Direct"),
+			new GUIContent("Unity", "Allows the AudioOutput component to grab audio from the video and play it through Unity to the AudioListener"),
+			new GUIContent("Facebook Audio 360", "Initialises player with Facebook Audio 360 support"),
+			new GUIContent("None", "No audio"),
+		};
+
+		private readonly static GUIContent[] _audioModesUWP =
+		{
+			new GUIContent("System Direct"),
+			new GUIContent("Unity", "Allows the AudioOutput component to grab audio from the video and play it through Unity to the AudioListener"),
+			new GUIContent("Facebook Audio 360", "Initialises player with Facebook Audio 360 support"),
+			new GUIContent("None", "No audio"),
+		};
+
+		private readonly static FieldDescription _optionLowLatency = new FieldDescription(".useLowLatency", new GUIContent("Use Low Latency", "Provides a hint to the decoder to use less buffering - may degrade performance and quality"));
+		private readonly static FieldDescription _optionVideoAPI = new FieldDescription(".videoApi", new GUIContent("Video API", "The preferred video API to use"));
+		private readonly static FieldDescription _optionTextureMips = new FieldDescription(".useTextureMips", new GUIContent("Generate Mipmaps", "Automatically create mip-maps for the texture to reducing aliasing when texture is scaled down"));
+		private readonly static FieldDescription _option10BitTextures = new FieldDescription(".use10BitTextures", new GUIContent("Use 10-Bit Textures", "Provides a hint to the decoder to use 10-bit textures - allowing more quality for videos encoded with a 10-bit profile"));
+		private readonly static FieldDescription _optionUseHardwareDecoding = new FieldDescription(".useHardwareDecoding", new GUIContent("Hardware Decoding"));
+		private readonly static FieldDescription _optionUseStereoDetection = new FieldDescription(".useStereoDetection", new GUIContent("Use Stereo Detection", "Disable if no stereo detection is required"));
+		private readonly static FieldDescription _optionUseTextTrackSupport = new FieldDescription(".useTextTrackSupport", new GUIContent("Use Text Tracks", "Disable if no text tracks are required"));
+		private readonly static FieldDescription _optionUseAudioDelay = new FieldDescription(".useAudioDelay", new GUIContent("Use Audio Delay", "Allows audio to be offset"));
+		private readonly static FieldDescription _optionUseFacebookAudio360Support = new FieldDescription(".useFacebookAudio360Support", new GUIContent("Use Facebook Audio 360", "Disable if no Facebook Audio 360 support is required for"));
+#if AVPROVIDEO_SUPPORT_BUFFERED_DISPLAY
+		private readonly static FieldDescription _optionPauseOnPrerollComplete = new FieldDescription(".pauseOnPrerollComplete", new GUIContent("Pause On Preroll Complete", "Internally pause once preroll is completed.  This is useful for syncing video playback to make sure all players are prerolled"));
+		private readonly static FieldDescription _optionBufferedFrameSelection = new FieldDescription(".bufferedFrameSelection", new GUIContent("Frame Selection", "Mode for selecting the next frame to display from the buffer fo frames"));
+#endif
+		private readonly static FieldDescription _optionUseHapNotchLC = new FieldDescription(".useHapNotchLC", new GUIContent("Use Hap/NotchLC", "Disable if no Hap/NotchLC playback is required"));
+		private readonly static FieldDescription _optionCustomMovParser = new FieldDescription(".useCustomMovParser", new GUIContent("Use Custom MOV Parser", "For playback of Hap and NotchLC media to handle high bit-rates"));
+		private readonly static FieldDescription _optionParallelFrameCount = new FieldDescription(".parallelFrameCount", new GUIContent("Parallel Frame Count", "Number of frames to decode in parallel via multi-threading.  Higher values increase latency but can improve performance for demanding videos."));
+		private readonly static FieldDescription _optionPrerollFrameCount = new FieldDescription(".prerollFrameCount", new GUIContent("Preroll Frame Count", "Number of frames to pre-decode before playback starts.  Higher values increase latency but can improve performance for demanding videos."));
+		private readonly static FieldDescription _optionAudioOutput = new FieldDescription(".audioOutput", new GUIContent("Audio Output"));
+		private readonly static FieldDescription _optionAudio360ChannelMode = new FieldDescription(".audio360ChannelMode", new GUIContent("Channel Mode", "Specifies what channel mode Facebook Audio 360 needs to be initialised with"));
+		private readonly static FieldDescription _optionStartMaxBitrate = new FieldDescription(".startWithHighestBitrate", new GUIContent("Start Max Bitrate"));
+		private readonly static FieldDescription _optionUseLowLiveLatency = new FieldDescription(".useLowLiveLatency", new GUIContent("Low Live Latency"));
+		private readonly static FieldDescription _optionHintAlphaChannel = new FieldDescription(".hintAlphaChannel", new GUIContent("Alpha Channel Hint", "If a video is detected as 32-bit, use or ignore the alpha channel"));
+		private readonly static FieldDescription _optionForceAudioOutputDeviceName = new FieldDescription(".forceAudioOutputDeviceName", new GUIContent("Force Audio Output Device Name", "Useful for VR when you need to output to the VR audio device"));
+		private readonly static FieldDescription _optionPreferredFilters = new FieldDescription(".preferredFilters", new GUIContent("Preferred Filters", "Priority list for preferred filters to be used instead of default"));
+
+		private void OnInspectorGUI_Override_Windows()
+		{
+			//MediaPlayer media = (this.target) as MediaPlayer;
+			//MediaPlayer.OptionsWindows options = media._optionsWindows;
+
+			GUILayout.Space(8f);
+
+			EditorGUILayout.BeginVertical(GUI.skin.box);
+			string optionsVarName = MediaPlayer.GetPlatformOptionsVariable(Platform.Windows);
+
+			{
+				SerializedProperty propVideoApi = DisplayPlatformOption(optionsVarName, _optionVideoAPI);
+				{
+					SerializedProperty propUseTextureMips = DisplayPlatformOption(optionsVarName, _optionTextureMips);
+					if (propUseTextureMips.boolValue && ((FilterMode)_propFilter.enumValueIndex) != FilterMode.Trilinear)
+					{
+						EditorHelper.IMGUI.NoticeBox(MessageType.Info, "Recommend changing the texture filtering mode to Trilinear when using mip-maps.");
+					}
+				}
+				{
+					SerializedProperty propUseHardwareDecoding = serializedObject.FindProperty(optionsVarName + _optionUseHardwareDecoding.fieldName);
+					EditorGUI.BeginDisabledGroup(!propUseHardwareDecoding.boolValue && propVideoApi.enumValueIndex == (int)Windows.VideoApi.MediaFoundation);
+					{
+						DisplayPlatformOption(optionsVarName, _option10BitTextures);
+					}
+					EditorGUI.EndDisabledGroup();
+				}
+			}
+			EditorGUILayout.EndVertical();
+
+			// Media Foundation Options
+			{
+				EditorGUILayout.BeginVertical(GUI.skin.box);
+				GUILayout.Label("Media Foundation API Options", EditorStyles.boldLabel);
+				{
+					DisplayPlatformOption(optionsVarName, _optionUseHardwareDecoding);
+				}
+				{
+					DisplayPlatformOption(optionsVarName, _optionLowLatency);
+					DisplayPlatformOption(optionsVarName, _optionUseStereoDetection);
+					DisplayPlatformOption(optionsVarName, _optionUseTextTrackSupport);
+#if AVPROVIDEO_SUPPORT_BUFFERED_DISPLAY
+					if (_showUltraOptions)
+					{
+						SerializedProperty propBufferedFrameSelection = DisplayPlatformOption(optionsVarName, _optionBufferedFrameSelection);
+						if (propBufferedFrameSelection.enumValueIndex != (int)BufferedFrameSelectionMode.None)
+						{
+							EditorGUI.indentLevel++;
+							DisplayPlatformOption(optionsVarName, _optionPauseOnPrerollComplete);
+							EditorGUI.indentLevel--;
+						}
+					}
+#endif
+					if (_showUltraOptions)
+					{
+						SerializedProperty useHapNotchLC = DisplayPlatformOption(optionsVarName, _optionUseHapNotchLC);
+						if (useHapNotchLC.boolValue)
+						{
+							EditorGUI.indentLevel++;
+							DisplayPlatformOption(optionsVarName, _optionCustomMovParser);
+							DisplayPlatformOption(optionsVarName, _optionParallelFrameCount);
+							DisplayPlatformOption(optionsVarName, _optionPrerollFrameCount);
+							EditorGUI.indentLevel--;
+						}
+					}
+				}
+				// Audio Output
+				{
+					SerializedProperty propAudioDelay = DisplayPlatformOption(optionsVarName, _optionUseAudioDelay);
+					if (propAudioDelay.boolValue)
+					{
+						//EditorGUI.indentLevel++;
+						//EditorGUI.indentLevel--;
+					}
+					DisplayPlatformOption(optionsVarName, _optionUseFacebookAudio360Support);
+					SerializedProperty propAudioOutput = DisplayPlatformOptionEnum(optionsVarName, _optionAudioOutput, _audioModesWindows);
+					if (_showUltraOptions && (Windows.AudioOutput)propAudioOutput.enumValueIndex == Windows.AudioOutput.FacebookAudio360)
+					{
+						EditorGUILayout.Space();
+						EditorGUILayout.LabelField("Facebook Audio 360", EditorStyles.boldLabel);
+					
+						DisplayPlatformOptionEnum(optionsVarName, _optionAudio360ChannelMode, _audio360ChannelMapGuiNames);
+
+						{
+							SerializedProperty propForceAudioOutputDeviceName = serializedObject.FindProperty(optionsVarName + ".forceAudioOutputDeviceName");
+							if (propForceAudioOutputDeviceName != null)
+							{
+								string[] deviceNames = { "Default", Windows.AudioDeviceOutputName_Rift, Windows.AudioDeviceOutputName_Vive, "Custom" };
+								int index = 0;
+								if (!string.IsNullOrEmpty(propForceAudioOutputDeviceName.stringValue))
+								{
+									switch (propForceAudioOutputDeviceName.stringValue)
+									{
+										case Windows.AudioDeviceOutputName_Rift:
+											index = 1;
+											break;
+										case Windows.AudioDeviceOutputName_Vive:
+											index = 2;
+											break;
+										default:
+											index = 3;
+											break;
+									}
+								}
+								int newIndex = EditorGUILayout.Popup("Audio Device Name", index, deviceNames);
+								if (newIndex == 0)
+								{
+									propForceAudioOutputDeviceName.stringValue = string.Empty;
+								}
+								else if (newIndex == 3)
+								{
+									if (index != newIndex)
+									{
+										if (string.IsNullOrEmpty(propForceAudioOutputDeviceName.stringValue) ||
+												propForceAudioOutputDeviceName.stringValue == Windows.AudioDeviceOutputName_Rift ||
+												propForceAudioOutputDeviceName.stringValue == Windows.AudioDeviceOutputName_Vive)
+										{
+											propForceAudioOutputDeviceName.stringValue = "?";
+										}
+									}
+									EditorGUILayout.PropertyField(propForceAudioOutputDeviceName, new GUIContent("Audio Device Name", "Useful for VR when you need to output to the VR audio device"));
+								}
+								else
+								{
+									propForceAudioOutputDeviceName.stringValue = deviceNames[newIndex];
+								}
+							}
+						}
+					}
+				}
+				EditorGUILayout.EndVertical();
+			}
+
+			// WinRT Options
+			{
+				EditorGUILayout.BeginVertical(GUI.skin.box);
+				GUILayout.Label("WinRT API Options", EditorStyles.boldLabel);
+				DisplayPlatformOption(optionsVarName, _optionStartMaxBitrate);
+				DisplayPlatformOption(optionsVarName, _optionUseLowLiveLatency);
+				if (_showUltraOptions)
+				{
+					SerializedProperty httpHeadersProp = serializedObject.FindProperty(optionsVarName + ".httpHeaders.httpHeaders");
+					if (httpHeadersProp != null)
+					{
+						OnInspectorGUI_HttpHeaders(httpHeadersProp);
+					}
+					GUILayout.Space(8f);
+					SerializedProperty keyAuthProp = serializedObject.FindProperty(optionsVarName + ".keyAuth");
+					if (keyAuthProp != null)
+					{
+						OnInspectorGUI_HlsDecryption(keyAuthProp);
+					}
+				}
+				EditorGUILayout.EndVertical();
+			}
+
+			// DirectShow Options
+			{
+				EditorGUILayout.BeginVertical(GUI.skin.box);
+				GUILayout.Label("DirectShow API Options", EditorStyles.boldLabel);
+
+				DisplayPlatformOption(optionsVarName, _optionHintAlphaChannel);
+				DisplayPlatformOption(optionsVarName, _optionForceAudioOutputDeviceName);
+				
+				{
+					int prevIndentLevel = EditorGUI.indentLevel;
+					EditorGUI.indentLevel = 1;
+					SerializedProperty propPreferredFilter = DisplayPlatformOption(optionsVarName, _optionPreferredFilters);
+					if (propPreferredFilter.arraySize > 0)
+					{
+						EditorHelper.IMGUI.NoticeBox(MessageType.Info, "Command filter names are:\n1) \"Microsoft DTV-DVD Video Decoder\" (best for compatibility when playing H.264 videos)\n2) \"LAV Video Decoder\"\n3) \"LAV Audio Decoder\"");
+					}
+					EditorGUI.indentLevel = prevIndentLevel;
+				}
+				EditorGUILayout.EndVertical();
+			}
+		}
+
+
+		private void OnInspectorGUI_Override_WindowsUWP()
+		{
+			//MediaPlayer media = (this.target) as MediaPlayer;
+			//MediaPlayer.OptionsWindowsUWP options = media._optionsWindowsUWP;
+
+			GUILayout.Space(8f);
+
+			string optionsVarName = MediaPlayer.GetPlatformOptionsVariable(Platform.WindowsUWP);
+
+			EditorGUILayout.BeginVertical(GUI.skin.box);
+
+			if (_showUltraOptions)
+			{
+				SerializedProperty propVideoApi = DisplayPlatformOption(optionsVarName, _optionVideoAPI);
+				{
+					SerializedProperty propUseHardwareDecoding = serializedObject.FindProperty(optionsVarName + _optionUseHardwareDecoding.fieldName);
+					EditorGUI.BeginDisabledGroup(!propUseHardwareDecoding.boolValue && propVideoApi.enumValueIndex == (int)Windows.VideoApi.MediaFoundation);
+					{
+						DisplayPlatformOption(optionsVarName, _option10BitTextures);
+					}
+					EditorGUI.EndDisabledGroup();
+				}
+			}
+
+			EditorGUILayout.EndVertical();
+
+			// Media Foundation Options
+			{
+				EditorGUILayout.BeginVertical(GUI.skin.box);
+				GUILayout.Label("Media Foundation API Options", EditorStyles.boldLabel);
+
+				DisplayPlatformOption(optionsVarName, _optionUseHardwareDecoding);
+				
+				{
+					SerializedProperty propUseTextureMips = DisplayPlatformOption(optionsVarName, _optionTextureMips);
+					if (propUseTextureMips.boolValue && ((FilterMode)_propFilter.enumValueIndex) != FilterMode.Trilinear)
+					{
+						EditorHelper.IMGUI.NoticeBox(MessageType.Info, "Recommend changing the texture filtering mode to Trilinear when using mip-maps.");
+					}
+				}
+
+				DisplayPlatformOption(optionsVarName, _optionLowLatency);
+
+				DisplayPlatformOptionEnum(optionsVarName, _optionAudioOutput, _audioModesUWP);
+
+				EditorGUILayout.EndVertical();
+			}
+
+			// WinRT Options
+			{
+				EditorGUILayout.BeginVertical(GUI.skin.box);
+				GUILayout.Label("WinRT API Options", EditorStyles.boldLabel);
+				
+				DisplayPlatformOption(optionsVarName, _optionStartMaxBitrate);
+				DisplayPlatformOption(optionsVarName, _optionUseLowLiveLatency);
+
+				if (_showUltraOptions)
+				{
+					{
+						SerializedProperty httpHeadersProp = serializedObject.FindProperty(optionsVarName + ".httpHeaders.httpHeaders");
+						if (httpHeadersProp != null)
+						{
+							OnInspectorGUI_HttpHeaders(httpHeadersProp);
+						}
+					}
+
+					{
+						SerializedProperty keyAuthProp = serializedObject.FindProperty(optionsVarName + ".keyAuth");
+						if (keyAuthProp != null)
+						{
+							OnInspectorGUI_HlsDecryption(keyAuthProp);
+						}
+					}
+				}
+				EditorGUILayout.EndVertical();
+			}
+
+			GUI.enabled = true;
+		}
+	}
+}

+ 12 - 0
package/Editor/Scripts/Components/MediaPlayerEditor_Windows.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: f96124fabf3e8cb46a512e3ecdbd6a3c
+timeCreated: 1448902492
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 517 - 0
package/Editor/Scripts/Components/PlaylistMediaPlayerEditor.cs

@@ -0,0 +1,517 @@
+using UnityEngine;
+using UnityEditor;
+using System.Collections.Generic;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// Editor for the MediaPlaylist class
+	/// </summary>
+	[CustomPropertyDrawer(typeof(MediaPlaylist))]
+	public class MediaPlaylistDrawer : PropertyDrawer
+	{
+		private static readonly GUIContent _guiTextInsert = new GUIContent("Clone");
+		private static readonly GUIContent _guiTextDelete = new GUIContent("Delete");
+		private static readonly GUIContent _guiTextUp = new GUIContent("↑");
+		private static readonly GUIContent _guiTextDown = new GUIContent("↓");
+		private static GUIStyle _styleButtonFoldout = null;
+		private static GUIStyle _styleHelpBoxNoPad = null;
+
+		public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
+		{
+			if (_styleButtonFoldout == null)
+			{
+				_styleButtonFoldout = new GUIStyle(EditorStyles.foldout);
+				_styleButtonFoldout.margin = new RectOffset();
+				_styleButtonFoldout.fontStyle = FontStyle.Bold;
+				_styleButtonFoldout.alignment = TextAnchor.MiddleLeft;
+			}
+			if (_styleHelpBoxNoPad == null)
+			{
+				_styleHelpBoxNoPad = new GUIStyle(EditorStyles.helpBox);
+				_styleHelpBoxNoPad.padding = new RectOffset();
+				_styleHelpBoxNoPad.overflow = new RectOffset();
+				_styleHelpBoxNoPad.margin = new RectOffset();
+				_styleHelpBoxNoPad.margin = new RectOffset(0, 0, 0, 0);
+				_styleHelpBoxNoPad.stretchWidth = false;
+				_styleHelpBoxNoPad.stretchHeight = false;
+			}
+
+			// Using BeginProperty / EndProperty on the parent property means that
+			// prefab override logic works on the entire property.
+			EditorGUI.BeginProperty(position, label, property);
+
+			SerializedProperty propItems = property.FindPropertyRelative("_items");
+
+			if (propItems.arraySize == 0)
+			{
+				if (GUILayout.Button("Insert Item"))
+				{
+					propItems.InsertArrayElementAtIndex(0);
+				}
+			}
+
+			int insertIndex = -1;
+			int deleteIndex = -1;
+
+			for (int i = 0; i < propItems.arraySize; i++)
+			{
+				SerializedProperty propItem = propItems.GetArrayElementAtIndex(i);
+
+				GUILayout.BeginVertical(_styleHelpBoxNoPad);
+				
+				GUI.backgroundColor = propItem.isExpanded ? Color.yellow : Color.white;
+				GUILayout.Box(GUIContent.none, EditorStyles.miniButton, GUILayout.ExpandWidth(true));
+				Rect buttonRect = GUILayoutUtility.GetLastRect();
+				GUI.backgroundColor = Color.white;
+				if (Event.current.type != EventType.Layout)
+				{
+					EditorGUI.indentLevel++;
+					SerializedProperty propName = propItem.FindPropertyRelative("name");
+					propItem.isExpanded = EditorGUI.Foldout(buttonRect, propItem.isExpanded, "#" + i + ": " + propName.stringValue, true, _styleButtonFoldout);
+					EditorGUI.indentLevel--;
+				}
+
+				GUILayout.BeginHorizontal();
+				GUILayout.FlexibleSpace();
+				if (GUILayout.Button(_guiTextInsert, GUILayout.ExpandWidth(false)))
+				{
+					insertIndex = i;
+					
+				}
+				if (GUILayout.Button(_guiTextDelete, GUILayout.ExpandWidth(false)))
+				{
+					deleteIndex = i;
+				}
+				EditorGUI.BeginDisabledGroup((i - 1) < 0);
+				if (GUILayout.Button(_guiTextUp, GUILayout.ExpandWidth(false)))
+				{
+					propItems.MoveArrayElement(i, i - 1);
+				}
+				EditorGUI.EndDisabledGroup();
+				EditorGUI.BeginDisabledGroup((i + 1) >= propItems.arraySize);
+				if (GUILayout.Button(_guiTextDown, GUILayout.ExpandWidth(false)))
+				{
+					propItems.MoveArrayElement(i, i + 1);
+				}
+				EditorGUI.EndDisabledGroup();
+				GUILayout.EndHorizontal();
+
+				if (propItem.isExpanded)
+				{
+					EditorGUILayout.PropertyField(propItem);
+				}
+
+				GUILayout.EndVertical();
+
+				GUILayout.Space(8f);
+			}
+
+			if (insertIndex >= 0)
+			{
+				propItems.InsertArrayElementAtIndex(insertIndex);
+			}
+			else if (deleteIndex >= 0)
+			{
+				propItems.DeleteArrayElementAtIndex(deleteIndex);
+			}
+
+			EditorGUI.EndProperty();
+		}
+	}
+
+	/// <summary>
+	/// Editor for the MediaPlaylist.MediaItem class
+	/// </summary>
+	[CustomPropertyDrawer(typeof(MediaPlaylist.MediaItem))]
+	public class MediaPlaylistItemDrawer : PropertyDrawer
+	{
+		private static readonly GUIContent _guiTextTransition = new GUIContent("Transition");
+		private static readonly GUIContent _guiTextOverrideTransition = new GUIContent("Override Transition");
+		private static readonly GUIContent _guiTextDuration = new GUIContent("Duration");
+		private static readonly GUIContent _guiTextEasing = new GUIContent("Easing");
+		
+		public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
+		{
+			EditorGUILayout.PropertyField(property.FindPropertyRelative("name"));
+			SerializedProperty propSourceType = property.FindPropertyRelative("sourceType");
+
+			EditorGUILayout.PropertyField(propSourceType);
+			if (propSourceType.enumValueIndex == 0)
+			{
+				EditorGUILayout.PropertyField(property.FindPropertyRelative("mediaPath"));
+				MediaPathDrawer.ShowBrowseButton(property.FindPropertyRelative("mediaPath"));
+			}
+			else
+			{
+				//EditorGUILayout.PropertyField(property.FindPropertyRelative("texture"));
+				//EditorGUILayout.PropertyField(property.FindPropertyRelative("textureDuration"));
+			}
+
+			EditorGUILayout.Space();
+
+			//EditorGUILayout.PropertyField(property.FindPropertyRelative("stereoPacking"));
+			//EditorGUILayout.PropertyField(property.FindPropertyRelative("alphaPacking"));
+
+			EditorGUILayout.Space();
+
+			EditorGUILayout.PropertyField(property.FindPropertyRelative("loop"));
+			EditorGUILayout.PropertyField(property.FindPropertyRelative("startMode"));
+			SerializedProperty propProgressMode = property.FindPropertyRelative("progressMode");
+			EditorGUILayout.PropertyField(propProgressMode);
+			if (propProgressMode.enumValueIndex == (int)PlaylistMediaPlayer.ProgressMode.BeforeFinish)
+			{
+				EditorGUILayout.PropertyField(property.FindPropertyRelative("progressTimeSeconds"));
+			}
+
+			EditorGUILayout.Space();
+
+			SerializedProperty propIsOverrideTransition = property.FindPropertyRelative("isOverrideTransition");
+			EditorGUILayout.PropertyField(propIsOverrideTransition, _guiTextOverrideTransition);
+			if (propIsOverrideTransition.boolValue)
+			{
+				EditorGUI.indentLevel++;
+				SerializedProperty propTransitionMode = property.FindPropertyRelative("overrideTransition");
+				EditorGUILayout.PropertyField(propTransitionMode, _guiTextTransition);
+				if (propTransitionMode.enumValueIndex != (int)PlaylistMediaPlayer.Transition.None)
+				{
+					EditorGUILayout.PropertyField(property.FindPropertyRelative("overrideTransitionDuration"), _guiTextDuration);
+					EditorGUILayout.PropertyField(property.FindPropertyRelative("overrideTransitionEasing"), _guiTextEasing);
+				}
+				EditorGUI.indentLevel--;
+			}
+		}
+	}
+
+	/// <summary>
+	/// Editor for the PlaylistMediaPlayer component
+	/// </summary>
+	[CanEditMultipleObjects]
+	[CustomEditor(typeof(PlaylistMediaPlayer))]
+	public class PlaylistMediaPlayerEditor : UnityEditor.Editor
+	{
+		private SerializedProperty _propPlayerA;
+		private SerializedProperty _propPlayerB;
+		private SerializedProperty _propPlaylist;
+		private SerializedProperty _propPlaylistAutoProgress;
+		private SerializedProperty _propAutoCloseVideo;
+		private SerializedProperty _propPlaylistLoopMode;
+		private SerializedProperty _propPausePreviousOnTransition;
+		private SerializedProperty _propDefaultTransition;
+		private SerializedProperty _propDefaultTransitionDuration;
+		private SerializedProperty _propDefaultTransitionEasing;
+		private SerializedProperty _propAudioVolume;
+		private SerializedProperty _propAudioMuted;
+		
+		private static bool _expandPlaylistItems = false;
+
+		private static Material _materialIMGUI = null;
+		private static GUIStyle _sectionBoxStyle = null;
+
+		private const string SettingsPrefix = "AVProVideo-PlaylistMediaPlayerEditor-";
+
+		private void OnEnable()
+		{
+			_propPlayerA = this.CheckFindProperty("_playerA");
+			_propPlayerB = this.CheckFindProperty("_playerB");
+			_propDefaultTransition = this.CheckFindProperty("_defaultTransition");
+			_propDefaultTransitionDuration = this.CheckFindProperty("_defaultTransitionDuration");
+			_propDefaultTransitionEasing = this.CheckFindProperty("_defaultTransitionEasing");
+			_propPausePreviousOnTransition = this.CheckFindProperty("_pausePreviousOnTransition");
+			_propPlaylist = this.CheckFindProperty("_playlist");
+			_propPlaylistAutoProgress = this.CheckFindProperty("_playlistAutoProgress");
+			_propAutoCloseVideo = this.CheckFindProperty("_autoCloseVideo");
+			_propPlaylistLoopMode = this.CheckFindProperty("_playlistLoopMode");
+			_propAudioVolume = this.CheckFindProperty("_playlistAudioVolume");
+			_propAudioMuted = this.CheckFindProperty("_playlistAudioMuted");
+
+			_expandPlaylistItems = EditorPrefs.GetBool(SettingsPrefix + "ExpandPlaylistItems", false);
+		}
+
+		private void OnDisable()
+		{
+			EditorPrefs.SetBool(SettingsPrefix + "ExpandPlaylistItems", _expandPlaylistItems);
+			if (_materialIMGUI)
+			{
+				DestroyImmediate(_materialIMGUI); _materialIMGUI = null;
+			}
+		}
+
+		public override bool RequiresConstantRepaint()
+		{
+			PlaylistMediaPlayer media = (this.target) as PlaylistMediaPlayer;
+			return (media.Control != null && media.isActiveAndEnabled);
+		}
+
+		public override void OnInspectorGUI()
+		{
+			PlaylistMediaPlayer media = (this.target) as PlaylistMediaPlayer;
+
+			serializedObject.Update();
+
+			if (media == null || _propPlayerA == null)
+			{
+				return;
+			}
+
+			if (_sectionBoxStyle == null)
+			{
+				_sectionBoxStyle = new GUIStyle(GUI.skin.box);
+				_sectionBoxStyle.padding.top = 0;
+				_sectionBoxStyle.padding.bottom = 0;
+			}
+
+			EditorGUILayout.PropertyField(_propPlayerA);
+			EditorGUILayout.PropertyField(_propPlayerB);
+			EditorGUILayout.Space();
+			EditorGUILayout.Space();
+			GUILayout.Label("Audio", EditorStyles.boldLabel);
+			EditorGUI.BeginChangeCheck();
+			EditorGUILayout.PropertyField(_propAudioVolume, new GUIContent("Volume"));
+			if (EditorGUI.EndChangeCheck())
+			{
+				foreach (PlaylistMediaPlayer player in this.targets)
+				{
+					player.AudioVolume = _propAudioVolume.floatValue;
+				}
+			}
+			EditorGUI.BeginChangeCheck();
+			EditorGUILayout.PropertyField(_propAudioMuted, new GUIContent("Muted"));
+			if (EditorGUI.EndChangeCheck())
+			{
+				foreach (PlaylistMediaPlayer player in this.targets)
+				{
+					player.AudioMuted = _propAudioMuted.boolValue;
+				}
+			}
+			EditorGUILayout.Space();
+			EditorGUILayout.Space();
+			GUILayout.Label("Playlist", EditorStyles.boldLabel);
+			EditorGUILayout.PropertyField(_propPlaylistAutoProgress, new GUIContent("Auto Progress"));
+			EditorGUILayout.PropertyField(_propPlaylistLoopMode, new GUIContent("Loop Mode"));
+			EditorGUILayout.PropertyField(_propAutoCloseVideo);
+
+			{
+				EditorGUILayout.Space();
+				EditorGUILayout.Space();
+				GUI.color = Color.white;
+				GUI.backgroundColor = Color.clear;
+				if (_expandPlaylistItems)
+				{
+					GUI.color = Color.white;
+					GUI.backgroundColor = new Color(0.8f, 0.8f, 0.8f, 0.1f);
+					if (EditorGUIUtility.isProSkin)
+					{
+						GUI.backgroundColor = Color.black;
+					}
+				}
+				GUILayout.BeginVertical(_sectionBoxStyle);
+				GUI.backgroundColor = Color.white;
+				if (GUILayout.Button("Playlist Items", EditorStyles.toolbarButton))
+				{
+					_expandPlaylistItems = !_expandPlaylistItems;
+				}
+				GUI.color = Color.white;
+
+				if (_expandPlaylistItems)
+				{	
+					EditorGUILayout.PropertyField(_propPlaylist);
+				}
+				GUILayout.EndVertical();
+			}
+			EditorGUILayout.Space(); 
+			EditorGUILayout.Space();
+			GUILayout.Label("Default Transition", EditorStyles.boldLabel);
+			EditorGUILayout.PropertyField(_propDefaultTransition, new GUIContent("Transition"));
+			EditorGUILayout.PropertyField(_propDefaultTransitionEasing, new GUIContent("Easing"));
+			EditorGUILayout.PropertyField(_propDefaultTransitionDuration, new GUIContent("Duration"));
+			EditorGUILayout.PropertyField(_propPausePreviousOnTransition, new GUIContent("Pause Previous"));
+			EditorGUILayout.Space();
+			EditorGUILayout.Space();
+
+			if (Application.isPlaying)
+			{
+				ITextureProducer textureSource = media.TextureProducer;
+
+				Texture texture = null;
+				if (textureSource != null)
+				{
+					texture = textureSource.GetTexture();
+				}
+				if (texture == null)
+				{
+					texture = EditorGUIUtility.whiteTexture;
+				}
+
+				float ratio = 1f;// (float)texture.width / (float)texture.height;
+
+				// Reserve rectangle for texture
+				GUILayout.BeginHorizontal();
+				GUILayout.FlexibleSpace();
+				Rect textureRect;
+				if (texture != EditorGUIUtility.whiteTexture)
+				{
+					textureRect = GUILayoutUtility.GetRect(Screen.width / 2, Screen.width / 2, (Screen.width / 2) / ratio, (Screen.width / 2) / ratio);
+				}
+				else
+				{
+					textureRect = GUILayoutUtility.GetRect(1920f / 40f, 1080f / 40f);
+				}
+				GUILayout.FlexibleSpace();
+				GUILayout.EndHorizontal();
+
+				string rateText = "0";
+				string playerText = string.Empty;
+				if (media.Info != null)
+				{
+					rateText = media.Info.GetVideoDisplayRate().ToString("F2");
+					playerText = media.Info.GetPlayerDescription();
+				}
+
+				EditorGUILayout.LabelField("Display Rate", rateText);
+				EditorGUILayout.LabelField("Using", playerText);
+								
+				// Draw the texture
+				if (Event.current.type == EventType.Repaint)
+				{
+					Matrix4x4 prevMatrix = GUI.matrix;
+					if (textureSource != null && textureSource.RequiresVerticalFlip())
+					{
+						GUIUtility.ScaleAroundPivot(new Vector2(1f, -1f), new Vector2(0f, textureRect.y + (textureRect.height / 2f)));
+					}
+
+					GUI.color = Color.gray;
+					EditorGUI.DrawTextureTransparent(textureRect, Texture2D.blackTexture, ScaleMode.StretchToFill);
+					GUI.color = Color.white;
+
+					if (!GUI.enabled)
+					{
+						GUI.color = Color.grey;
+						GUI.DrawTexture(textureRect, texture, ScaleMode.ScaleToFit, false);
+						GUI.color = Color.white;
+					}
+					else
+					{
+						if (!_materialIMGUI)
+						{
+							_materialIMGUI = VideoRender.CreateResolveMaterial( false );
+							VideoRender.SetupGammaMaterial(_materialIMGUI, true);
+						}
+						{
+							EditorGUI.DrawPreviewTexture(textureRect, texture, _materialIMGUI, ScaleMode.ScaleToFit);
+						}
+					}
+					GUI.matrix = prevMatrix;
+				}
+			}
+
+			EditorGUI.BeginDisabledGroup(!(media.Control != null && media.Control.CanPlay() && media.isActiveAndEnabled && !EditorApplication.isPaused));
+			OnInspectorGUI_PlayControls(media);
+			EditorGUI.EndDisabledGroup();
+
+			EditorGUILayout.Space();
+			EditorGUILayout.Space();
+
+			EditorGUI.BeginDisabledGroup(!Application.isPlaying);
+
+			GUILayout.Label("Current Item: " + media.PlaylistIndex + " / " + Mathf.Max(0, media.Playlist.Items.Count - 1) );
+
+			GUILayout.BeginHorizontal();
+			EditorGUI.BeginDisabledGroup(!media.CanJumpToItem(media.PlaylistIndex - 1));
+			if (GUILayout.Button("Prev"))
+			{
+				media.PrevItem();
+			}
+			EditorGUI.EndDisabledGroup();
+			EditorGUI.BeginDisabledGroup(!media.CanJumpToItem(media.PlaylistIndex + 1));
+			if (GUILayout.Button("Next"))
+			{
+				media.NextItem();
+			}
+			EditorGUI.EndDisabledGroup();
+			GUILayout.EndHorizontal();
+			EditorGUI.EndDisabledGroup();
+
+			serializedObject.ApplyModifiedProperties();
+		}
+
+		private void OnInspectorGUI_PlayControls(PlaylistMediaPlayer player)
+		{
+			GUILayout.Space(8.0f);
+
+			// Slider
+			EditorGUILayout.BeginHorizontal();
+			bool isPlaying = false;
+			if (player.Control != null)
+			{
+				isPlaying = player.Control.IsPlaying();
+			}
+			float currentTime = 0f;
+			if (player.Control != null)
+			{
+				currentTime = (float)player.Control.GetCurrentTime();
+			}
+
+			float durationTime = 0f;
+			if (player.Info != null)
+			{
+				durationTime = (float)player.Info.GetDuration();
+				if (float.IsNaN(durationTime))
+				{
+					durationTime = 0f;
+				}
+			}
+			string timeUsed = Helper.GetTimeString(currentTime, true);
+			GUILayout.Label(timeUsed, GUILayout.ExpandWidth(false));
+
+			float newTime = GUILayout.HorizontalSlider(currentTime, 0f, durationTime, GUILayout.ExpandWidth(true));
+			if (newTime != currentTime && player.Control != null)
+			{
+				player.Control.Seek(newTime);
+			}
+
+			string timeTotal = "Infinity";
+			if (!float.IsInfinity(durationTime))
+			{
+				timeTotal = Helper.GetTimeString(durationTime, true);
+			}
+
+			GUILayout.Label(timeTotal, GUILayout.ExpandWidth(false));
+
+			EditorGUILayout.EndHorizontal();
+
+			// Buttons
+			EditorGUILayout.BeginHorizontal();
+			if (GUILayout.Button("Rewind", GUILayout.ExpandWidth(false)))
+			{
+				if (player.Control != null)
+				{
+					player.Control.Rewind();
+				}
+			}
+
+			if (!isPlaying)
+			{
+				GUI.color = Color.green;
+				if (GUILayout.Button("Play", GUILayout.ExpandWidth(true)))
+				{
+					player.Play();
+				}
+			}
+			else
+			{
+				GUI.color = Color.yellow;
+				if (GUILayout.Button("Pause", GUILayout.ExpandWidth(true)))
+				{
+					player.Pause();
+				}
+			}
+			GUI.color = Color.white;
+			EditorGUILayout.EndHorizontal();
+		}
+	}
+}

+ 8 - 0
package/Editor/Scripts/Components/PlaylistMediaPlayerEditor.cs.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: c9328411ef862884f97a993c4daa9b68
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 

+ 118 - 0
package/Editor/Scripts/Components/ResolveToRenderTextureEditor.cs

@@ -0,0 +1,118 @@
+using UnityEditor;
+using UnityEngine;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// Editor for the ResolveToRenderTexture component
+	/// </summary>
+	[CanEditMultipleObjects]
+	[CustomEditor(typeof(ResolveToRenderTexture))]
+	public class ResolveToRenderTextureEditor : UnityEditor.Editor
+	{
+		private SerializedProperty _propMediaPlayer;
+		private SerializedProperty _propExternalTexture;
+		private SerializedProperty _propResolveFlags;
+
+		private SerializedProperty _propOptionsApplyHSBC;
+		private SerializedProperty _propOptionsHue;
+		private SerializedProperty _propOptionsSaturation;
+		private SerializedProperty _propOptionsBrightness;
+		private SerializedProperty _propOptionsContrast;
+		private SerializedProperty _propOptionsGamma;
+		private SerializedProperty _propOptionsTint;
+
+		void OnEnable()
+		{
+			_propMediaPlayer = this.CheckFindProperty("_mediaPlayer");
+			_propExternalTexture = this.CheckFindProperty("_externalTexture");
+			_propResolveFlags = this.CheckFindProperty("_resolveFlags");
+			_propOptionsApplyHSBC = this.CheckFindProperty("_options.applyHSBC");
+			_propOptionsHue = this.CheckFindProperty("_options.hue");
+			_propOptionsSaturation = this.CheckFindProperty("_options.saturation");
+			_propOptionsBrightness = this.CheckFindProperty("_options.brightness");
+			_propOptionsContrast = this.CheckFindProperty("_options.contrast");
+			_propOptionsGamma = this.CheckFindProperty("_options.gamma");
+			_propOptionsTint = this.CheckFindProperty("_options.tint");
+		}
+
+		private void ButtonFloatReset(SerializedProperty prop, float value)
+		{
+			GUILayout.BeginHorizontal();
+			EditorGUILayout.PropertyField(prop);
+			if (GUILayout.Button("Reset", GUILayout.ExpandWidth(false)))
+			{
+				prop.floatValue = value;
+			}
+			GUILayout.EndHorizontal();
+		}
+
+		private void ButtonColorReset(SerializedProperty prop, Color value)
+		{
+			GUILayout.BeginHorizontal();
+			EditorGUILayout.PropertyField(prop);
+			if (GUILayout.Button("Reset", GUILayout.ExpandWidth(false)))
+			{
+				prop.colorValue = value;
+			}
+			GUILayout.EndHorizontal();
+		}
+
+		public override void OnInspectorGUI()
+		{
+			serializedObject.Update();
+
+			EditorGUILayout.PropertyField(_propMediaPlayer);
+			EditorGUILayout.PropertyField(_propExternalTexture);
+			_propResolveFlags.intValue = EditorGUILayout.MaskField("Resolve Flags", _propResolveFlags.intValue, System.Enum.GetNames(typeof( VideoRender.ResolveFlags)));
+
+			EditorGUI.BeginChangeCheck();
+			{
+				EditorGUILayout.PropertyField(_propOptionsApplyHSBC);
+				EditorGUI.BeginDisabledGroup(!_propOptionsApplyHSBC.boolValue);
+				{
+					EditorGUI.indentLevel++;
+					ButtonFloatReset(_propOptionsHue, 0f);
+					ButtonFloatReset(_propOptionsSaturation, 0.5f);
+					ButtonFloatReset(_propOptionsBrightness, 0.5f);
+					ButtonFloatReset(_propOptionsContrast, 0.5f);
+					ButtonFloatReset(_propOptionsGamma, 1f);
+					EditorGUI.indentLevel--;
+				}
+				EditorGUI.EndDisabledGroup();
+				ButtonColorReset(_propOptionsTint, Color.white);
+			}
+			if (EditorGUI.EndChangeCheck())
+			{
+				Object[] resolves = this.serializedObject.targetObjects;
+				if (resolves != null)
+				{
+					foreach (ResolveToRenderTexture resolve in resolves)
+					{
+						resolve.SetMaterialDirty();
+					}
+				}
+			}
+
+			serializedObject.ApplyModifiedProperties();
+
+			{
+				ResolveToRenderTexture resolve = this.target as ResolveToRenderTexture;
+				if (resolve != null && resolve.TargetTexture != null)
+				{
+					Rect r = GUILayoutUtility.GetAspectRect(resolve.TargetTexture.width / (float)resolve.TargetTexture.height);
+					GUI.DrawTexture(r, resolve.TargetTexture, ScaleMode.StretchToFill, true);
+					if (GUILayout.Button("Select Texture"))
+					{
+						Selection.activeObject = resolve.TargetTexture;
+					}
+					Repaint();
+				}
+			}
+		}
+	}
+}

+ 12 - 0
package/Editor/Scripts/Components/ResolveToRenderTextureEditor.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: b8e6745d22bfa424b83014fd0ed7bd29
+timeCreated: 1653302586
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 407 - 0
package/Editor/Scripts/EditorHelper.cs

@@ -0,0 +1,407 @@
+using UnityEngine;
+using UnityEditor;
+using System.Collections.Generic;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// Helper methods for editor components
+	/// </summary>
+	public static class EditorHelper
+	{
+		/// <summary>
+		/// Loads from EditorPrefs, converts a CSV string to List<string> and returns it
+		/// </summary>
+		internal static List<string> GetEditorPrefsToStringList(string key, char separator = ';')
+		{
+			string items = EditorPrefs.GetString(key, string.Empty);
+			return new List<string>(items.Split(new char[] { separator }, System.StringSplitOptions.RemoveEmptyEntries));
+		}
+
+		/// <summary>
+		/// Converts a List<string> into a CSV string and saves it in EditorPrefs
+		/// </summary>
+		internal static void SetEditorPrefsFromStringList(string key, List<string> items, char separator = ';')
+		{
+			string value = string.Empty;
+			if (items != null && items.Count > 0)
+			{
+				value = string.Join(separator.ToString(), items.ToArray());
+			}
+			EditorPrefs.SetString(key, value);
+		}
+
+		public static SerializedProperty CheckFindProperty(this UnityEditor.Editor editor, string propertyName)
+		{
+			SerializedProperty result = editor.serializedObject.FindProperty(propertyName);
+			Debug.Assert(result != null, "Missing property: " + propertyName);
+			return result;
+		}
+
+		/// <summary>
+		/// Only lets the property if the proposed path doesn't contain invalid characters
+		/// Also changes all backslash characters to forwardslash for better cross-platform compatability
+		/// </summary>
+		internal static bool SafeSetPathProperty(string path, SerializedProperty property)
+		{
+			bool result = false;
+			if (path == null)
+			{
+				path = string.Empty;
+			}
+			else if (path.IndexOfAny(System.IO.Path.GetInvalidPathChars()) < 0)
+			{
+				path = path.Replace("\\", "/");
+			}
+			if (path.StartsWith("//"))
+			{
+				path = path.Substring(2);
+			}
+
+			if (path != property.stringValue)
+			{
+				property.stringValue = path;
+				result = true;
+			}
+			
+			return result;
+		}
+
+		/// <summary>
+		/// Returns whether a define exists for a specific platform
+		/// </summary>
+		internal static bool HasScriptDefine(string define, BuildTargetGroup buildTarget = BuildTargetGroup.Unknown)
+		{
+			if (buildTarget == BuildTargetGroup.Unknown) { buildTarget = EditorUserBuildSettings.selectedBuildTargetGroup; }
+			string defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(buildTarget);
+			return defines.Contains(define);
+		}
+
+		/// <summary>
+		/// Adds a define if it doesn't already exist for a specific platform
+		/// </summary>
+		internal static void AddScriptDefine(string define, BuildTargetGroup buildTarget = BuildTargetGroup.Unknown)
+		{
+			if (buildTarget == BuildTargetGroup.Unknown) { buildTarget = EditorUserBuildSettings.selectedBuildTargetGroup; }
+			string defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(buildTarget);
+			if (!defines.Contains(define))
+			{
+				defines += ";" + define + ";";
+				PlayerSettings.SetScriptingDefineSymbolsForGroup(buildTarget, defines);
+			}
+		}
+
+		/// <summary>
+		/// Removes a define if it exists for a specific platform
+		/// </summary>
+		internal static void RemoveScriptDefine(string define, BuildTargetGroup buildTarget = BuildTargetGroup.Unknown)
+		{
+			if (buildTarget == BuildTargetGroup.Unknown) { buildTarget = EditorUserBuildSettings.selectedBuildTargetGroup; }
+			string defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(buildTarget);
+			if (defines.Contains(define))
+			{
+				defines = defines.Replace(define, "");
+				PlayerSettings.SetScriptingDefineSymbolsForGroup(buildTarget, defines);
+			}
+		}
+
+		/// <summary>
+		/// Given a partial file path and MediaLocation, return a directory path suitable for a file browse dialog to start in
+		/// </summary>
+		internal static string GetBrowsableFolder(string path, MediaPathType fileLocation)
+		{
+			// Try to resolve based on file path + file location
+			string result = Helper.GetFilePath(path, fileLocation);
+			if (!string.IsNullOrEmpty(result))
+			{
+				if (System.IO.File.Exists(result))
+				{
+					result = System.IO.Path.GetDirectoryName(result);
+				}
+			}
+
+			if (!System.IO.Directory.Exists(result))
+			{
+				// Just resolve on file location
+				result = Helper.GetPath(fileLocation);
+			}
+			if (string.IsNullOrEmpty(result))
+			{
+				// Fallback
+				result = Application.streamingAssetsPath;
+			}
+			return result;
+		}
+
+		internal static bool OpenMediaFileDialog(string startPath, ref MediaPath mediaPath, ref string fullPath, string extensions)
+		{
+			bool result = false;
+
+			string path = UnityEditor.EditorUtility.OpenFilePanel("Browse Media File", startPath, extensions);
+			if (!string.IsNullOrEmpty(path) && !path.EndsWith(".meta"))
+			{
+				mediaPath = GetMediaPathFromFullPath(path);
+				result = true;
+			}
+
+			return result;
+		}
+
+		/*private static bool IsPathWithin(string fullPath, string targetPath)
+		{
+			return fullPath.StartsWith(targetPath);
+		}*/
+
+		private static string GetPathRelativeTo(string root, string fullPath)
+		{
+			string result = fullPath.Remove(0, root.Length);
+			if (result.StartsWith(System.IO.Path.DirectorySeparatorChar.ToString()) || result.StartsWith(System.IO.Path.AltDirectorySeparatorChar.ToString()))
+			{
+				result = result.Remove(0, 1);
+			}
+			return result;
+		}
+
+		internal static MediaPath GetMediaPathFromFullPath(string fullPath)
+		{
+			MediaPath result = null;
+			string projectRoot = System.IO.Path.GetFullPath(System.IO.Path.Combine(Application.dataPath, ".."));
+			projectRoot = projectRoot.Replace('\\', '/');
+
+			if (fullPath.StartsWith(projectRoot))
+			{
+				if (fullPath.StartsWith(Application.streamingAssetsPath))
+				{
+					// Must be StreamingAssets relative path
+					result = new MediaPath(GetPathRelativeTo(Application.streamingAssetsPath, fullPath), MediaPathType.RelativeToStreamingAssetsFolder);
+				}
+				else if (fullPath.StartsWith(Application.dataPath))
+				{
+					// Must be Assets relative path
+					result = new MediaPath(GetPathRelativeTo(Application.dataPath, fullPath), MediaPathType.RelativeToDataFolder);
+				}
+				else
+				{
+					// Must be project relative path
+					result = new MediaPath(GetPathRelativeTo(projectRoot, fullPath), MediaPathType.RelativeToProjectFolder);
+				}
+			}
+			else
+			{
+				// Must be persistant data
+				if (fullPath.StartsWith(Application.persistentDataPath))
+				{
+					result = new MediaPath(GetPathRelativeTo(Application.persistentDataPath, fullPath), MediaPathType.RelativeToPersistentDataFolder);
+				}
+
+				// Must be absolute path
+				result = new MediaPath(fullPath, MediaPathType.AbsolutePathOrURL);
+			}
+			return result;
+		}
+
+		internal class IMGUI
+		{
+			private static GUIStyle _copyableStyle = null;
+			private static GUIStyle _wordWrappedTextAreaStyle = null;
+			private static GUIStyle _rightAlignedLabelStyle = null;
+			private static GUIStyle _centerAlignedLabelStyle = null;
+
+			/// <summary>
+			/// Displays an IMGUI warning text box inline
+			/// </summary>
+			internal static void WarningTextBox(string title, string body, Color bgColor, Color titleColor, Color bodyColor)
+			{
+				BeginWarningTextBox(title, body, bgColor, titleColor, bodyColor);
+				EndWarningTextBox();
+			}
+
+			/// <summary>
+			/// Displays an IMGUI warning text box inline
+			/// </summary>
+			internal static void BeginWarningTextBox(string title, string body, Color bgColor, Color titleColor, Color bodyColor)
+			{
+				GUI.backgroundColor = bgColor;
+				EditorGUILayout.BeginVertical(GUI.skin.box);
+				if (!string.IsNullOrEmpty(title))
+				{
+					GUI.color = titleColor;
+					GUILayout.Label(title, EditorStyles.boldLabel);
+				}
+				if (!string.IsNullOrEmpty(body))
+				{
+					GUI.color = bodyColor;
+					GUILayout.Label(body, EditorStyles.wordWrappedLabel);
+				}
+			}
+
+			internal static void EndWarningTextBox()
+			{
+				EditorGUILayout.EndVertical();
+				GUI.backgroundColor = Color.white;
+				GUI.color = Color.white;
+			}
+
+			/// <summary>
+			/// Displays an IMGUI box containing a copyable string that wraps
+			/// Usedful for very long strings eg file paths/urls
+			/// </summary>
+			internal static void CopyableFilename(string path)
+			{
+				// The box disappars unless it has some content
+				if (string.IsNullOrEmpty(path))
+				{
+					path = " ";
+				}
+
+				// Display the file name so it's easy to read and copy to the clipboard
+				if (!string.IsNullOrEmpty(path) && 0 > path.IndexOfAny(System.IO.Path.GetInvalidPathChars()))
+				{
+					// Some GUI hacks here because SelectableLabel wants to be double height and it doesn't want to be centered because it's an EditorGUILayout function...
+					string text = System.IO.Path.GetFileName(path);
+
+					if (_copyableStyle == null)
+					{
+						_copyableStyle = new GUIStyle(EditorStyles.wordWrappedLabel);
+						_copyableStyle.fontStyle = FontStyle.Bold;
+						_copyableStyle.stretchWidth = true;
+						_copyableStyle.stretchHeight = true;
+						_copyableStyle.alignment = TextAnchor.MiddleCenter;
+						_copyableStyle.margin.top = 8;
+						_copyableStyle.margin.bottom = 16;
+					}
+
+					float height = _copyableStyle.CalcHeight(new GUIContent(text), Screen.width)*1.5f;
+					EditorGUILayout.SelectableLabel(text, _copyableStyle, GUILayout.Height(height), GUILayout.ExpandHeight(false), GUILayout.ExpandWidth(true));
+				}
+			}
+
+			/// <summary>
+			/// </summary>
+			internal static GUIStyle GetWordWrappedTextAreaStyle()
+			{
+				if (_wordWrappedTextAreaStyle == null)
+				{
+					_wordWrappedTextAreaStyle = new GUIStyle(EditorStyles.textArea);
+					_wordWrappedTextAreaStyle.wordWrap = true;
+				}
+				return _wordWrappedTextAreaStyle;
+			}
+
+			internal static GUIStyle GetRightAlignedLabelStyle()
+			{
+				if (_rightAlignedLabelStyle == null)
+				{
+					_rightAlignedLabelStyle = new GUIStyle(GUI.skin.label);
+					_rightAlignedLabelStyle.alignment = TextAnchor.UpperRight;
+				}
+				return _rightAlignedLabelStyle;
+			}
+
+			internal static GUIStyle GetCenterAlignedLabelStyle()
+			{
+				if (_centerAlignedLabelStyle == null)
+				{
+					_centerAlignedLabelStyle = new GUIStyle(GUI.skin.label);
+					_centerAlignedLabelStyle.alignment = TextAnchor.MiddleCenter;
+				}
+				return _centerAlignedLabelStyle;
+			}			
+
+			/// <summary>
+			/// Displays IMGUI box in red/yellow for errors/warnings
+			/// </summary>
+			internal static void NoticeBox(MessageType messageType, string message)
+			{
+				//GUI.backgroundColor = Color.yellow;
+				//EditorGUILayout.HelpBox(message, messageType);
+
+				switch (messageType)
+				{
+					case MessageType.Error:
+						GUI.color = Color.red;
+						message = "Error: " + message;
+						break;
+					case MessageType.Warning:
+						GUI.color = Color.yellow;
+						message = "Warning: " + message;
+						break;
+				}
+
+				//GUI.color = Color.yellow;
+				GUILayout.TextArea(message);
+				GUI.color = Color.white;
+			}
+
+			/// <summary>
+			/// Displays IMGUI text centered horizontally
+			/// </summary>
+			internal static void CentreLabel(string text, GUIStyle style = null)
+			{
+				GUILayout.BeginHorizontal();
+				GUILayout.FlexibleSpace();
+				if (style == null)
+				{
+					GUILayout.Label(text);
+				}
+				else
+				{
+					GUILayout.Label(text, style);
+				}
+				GUILayout.FlexibleSpace();
+				GUILayout.EndHorizontal();
+			}
+
+			internal static bool ToggleScriptDefine(string label, string define)
+			{
+				EditorGUI.BeginChangeCheck();
+				bool isEnabled = EditorGUILayout.Toggle(label, EditorHelper.HasScriptDefine(define));
+				if (EditorGUI.EndChangeCheck())
+				{
+					if (isEnabled)
+					{
+						EditorHelper.AddScriptDefine(define);
+					}
+					else
+					{
+						EditorHelper.RemoveScriptDefine(define);
+					}
+				}
+				return isEnabled;
+			}
+		}
+	}
+
+	internal class HorizontalFlowScope : GUI.Scope
+	{
+		private float _windowWidth;
+		private float _width;
+
+		public HorizontalFlowScope(int windowWidth)
+		{
+			_windowWidth = windowWidth;
+			_width = _windowWidth;
+			GUILayout.BeginHorizontal();
+		}
+
+		protected override void CloseScope()
+		{
+			GUILayout.EndHorizontal();
+		}
+
+		public void AddItem(GUIContent content, GUIStyle style)
+		{
+			_width -= style.CalcSize(content).x + style.padding.horizontal;
+			if (_width <= 0f)
+			{
+				_width += Screen.width;
+				GUILayout.EndHorizontal();
+				GUILayout.BeginHorizontal();
+			}
+		}
+	}
+}

+ 12 - 0
package/Editor/Scripts/EditorHelper.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: d5f2bdbf8e8ad454482c409d00e673ed
+timeCreated: 1448902492
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 44 - 0
package/Editor/Scripts/MediaHintsDrawer.cs

@@ -0,0 +1,44 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEditor;
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	[CustomPropertyDrawer(typeof(MediaHints))]
+	public class MediaHintsDrawer : PropertyDrawer
+	{
+		private readonly static GUIContent[] StereoPackingOptions =
+		{
+			// NOTE: must be in the same order as enum StereoPacking
+			new GUIContent("None"),
+			new GUIContent("Top Bottom"),
+			new GUIContent("Left Right"),
+			new GUIContent("Custom UV"),
+		};
+
+		public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { return 0f; }
+
+		public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
+		{
+			EditorGUI.BeginProperty(position, GUIContent.none, property);
+
+			SerializedProperty propHintsTransparency = property.FindPropertyRelative("transparency");
+			SerializedProperty propHintsAlphaPacking = property.FindPropertyRelative("alphaPacking");
+			SerializedProperty propHintsStereoPacking = property.FindPropertyRelative("stereoPacking");
+
+			EditorGUILayout.PropertyField(propHintsTransparency);
+			if ((TransparencyMode)propHintsTransparency.enumValueIndex == TransparencyMode.Transparent)
+			{
+				EditorGUILayout.PropertyField(propHintsAlphaPacking);
+			}
+
+			{
+				// NOTE: We don't allow selection of 'Two Textures' as this mode is only produced by the Players as it is platform specific
+				propHintsStereoPacking.enumValueIndex = EditorGUILayout.Popup(new GUIContent("Stereo Packing"), propHintsStereoPacking.enumValueIndex, StereoPackingOptions);
+			}
+
+			EditorGUI.EndProperty();
+		}
+	}
+}

+ 12 - 0
package/Editor/Scripts/MediaHintsDrawer.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 46cb5084587db3f40ae472c68bceaf34
+timeCreated: 1614875697
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 62 - 0
package/Editor/Scripts/MediaPathDrawer.cs

@@ -0,0 +1,62 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEditor;
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	[CustomPropertyDrawer(typeof(MediaPath))]
+	public class MediaPathDrawer : PropertyDrawer
+	{
+		public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { return 0f; }
+
+		public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
+		{
+			EditorGUI.BeginProperty(position, GUIContent.none, property);
+
+			SerializedProperty propPath = property.FindPropertyRelative("_path");
+			SerializedProperty propPathType = property.FindPropertyRelative("_pathType");
+
+			EditorGUILayout.PropertyField(propPathType, GUIContent.none);
+
+			//GUI.color = HttpHeader.IsValid(valueProp.stringValue)?Color.white:Color.red;
+			string newUrl = EditorGUILayout.TextArea(propPath.stringValue, EditorHelper.IMGUI.GetWordWrappedTextAreaStyle());
+			//GUI.color = Color.white;
+			newUrl = newUrl.Trim();
+			if (EditorHelper.SafeSetPathProperty(newUrl, propPath))
+			{
+				// TODO: shouldn't we set all targets?
+				EditorUtility.SetDirty(property.serializedObject.targetObject);
+			}
+			MediaPlayerEditor.ShowFileWarningMessages(propPath.stringValue, (MediaPathType)propPathType.enumValueIndex, null, MediaSource.Path, false, Platform.Unknown);
+			GUI.color = Color.white;
+			
+			EditorGUI.EndProperty();
+		}
+
+		public static void ShowBrowseButton(SerializedProperty propMediaPath)
+		{
+			GUIContent buttonText = new GUIContent("Browse", EditorGUIUtility.IconContent("d_Project").image);
+			if (GUILayout.Button(buttonText, GUILayout.ExpandWidth(true)))
+			{
+				RecentMenu.Create(propMediaPath, null, MediaPlayerEditor.MediaFileExtensions, false);
+			}
+		}
+
+		public static void ShowBrowseButtonIcon(SerializedProperty propMediaPath, SerializedProperty propMediaSource)
+		{
+			if (GUILayout.Button(EditorGUIUtility.IconContent("d_Project"), GUILayout.ExpandWidth(false)))
+			{
+				RecentMenu.Create(propMediaPath, propMediaSource, MediaPlayerEditor.MediaFileExtensions, false, 100);
+			}
+		}
+
+		public static void ShowBrowseSubtitlesButtonIcon(SerializedProperty propMediaPath)
+		{
+			if (GUILayout.Button(EditorGUIUtility.IconContent("d_Project"), GUILayout.ExpandWidth(false)))// GUILayout.Height(EditorGUIUtility.singleLineHeight)))
+			{
+				RecentMenu.Create(propMediaPath, null, MediaPlayerEditor.SubtitleFileExtensions, false);
+			}
+		}
+	}
+}

+ 12 - 0
package/Editor/Scripts/MediaPathDrawer.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: d8604f79a3c54214dae18738c95583f7
+timeCreated: 1614875700
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 388 - 0
package/Editor/Scripts/MediaReferenceEditor.cs

@@ -0,0 +1,388 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEditor;
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	[CanEditMultipleObjects()]
+	[CustomEditor(typeof(MediaReference))]
+	public class MediaReferenceEditor : UnityEditor.Editor
+	{
+		internal const string SettingsPrefix = "AVProVideo-MediaReferenceEditor-";
+
+		private SerializedProperty _propMediaPath;
+		private SerializedProperty _propHints;
+
+		private SerializedProperty _propPlatformMacOS;
+		private SerializedProperty _propPlatformWindows;
+		private SerializedProperty _propPlatformAndroid;
+		private SerializedProperty _propPlatformIOS;
+		private SerializedProperty _propPlatformTvOS;
+		private SerializedProperty _propPlatformWindowsUWP;
+		private SerializedProperty _propPlatformWebGL;
+		//private SerializedProperty _propAlias;
+
+		void OnEnable()
+		{
+			_propMediaPath = this.CheckFindProperty("_mediaPath");
+			_propHints = this.CheckFindProperty("_hints");
+
+			_propPlatformMacOS = this.CheckFindProperty("_macOS");
+			_propPlatformWindows = this.CheckFindProperty("_windows");
+			_propPlatformAndroid = this.CheckFindProperty("_android");
+			_propPlatformIOS = this.CheckFindProperty("_iOS");
+			_propPlatformTvOS = this.CheckFindProperty("_tvOS");
+			_propPlatformWindowsUWP = this.CheckFindProperty("_windowsUWP");
+			_propPlatformWebGL = this.CheckFindProperty("_webGL");
+
+			//_propAlias = CheckFindProperty("_alias");
+			_zoomToFill = EditorPrefs.GetBool(SettingsPrefix + "ZoomToFill", _zoomToFill);
+			_thumbnailTime = EditorPrefs.GetFloat(SettingsPrefix + "ThumbnailTime", _thumbnailTime);
+		}
+
+		void OnDisable()
+		{
+			EndGenerateThumbnails(false);
+			RemoveProgress();
+
+			EditorPrefs.SetBool(SettingsPrefix + "ZoomToFill", _zoomToFill);
+			EditorPrefs.SetFloat(SettingsPrefix + "ThumbnailTime", _thumbnailTime);
+		}
+
+		public override void OnInspectorGUI()
+		{
+			//MediaPlayer media = (this.target) as MediaPlayer;
+
+			//this.DrawDefaultInspector();
+
+			serializedObject.Update();
+
+			GUILayout.Label("Media Reference");
+			EditorGUILayout.Space();
+			//EditorGUILayout.PropertyField(_propAlias);
+			//EditorGUILayout.Space();
+
+			{
+				string mediaName = _propMediaPath.FindPropertyRelative("_path").stringValue;	
+				GUILayout.BeginVertical(GUI.skin.box);
+				MediaPlayerEditor.OnInspectorGUI_CopyableFilename(mediaName);
+				GUILayout.EndVertical();
+			}
+
+			EditorGUILayout.PropertyField(_propMediaPath);
+
+			MediaPathDrawer.ShowBrowseButton(_propMediaPath);
+
+			EditorGUILayout.Space();
+
+			//GUILayout.Label("Media Hints", EditorStyles.boldLabel);
+			EditorGUILayout.PropertyField(_propHints);
+
+			EditorGUILayout.PropertyField(_propPlatformMacOS, new GUIContent("macOS"));
+			EditorGUILayout.PropertyField(_propPlatformWindows, new GUIContent("Windows"));
+			EditorGUILayout.PropertyField(_propPlatformAndroid, new GUIContent("Android"));
+			EditorGUILayout.PropertyField(_propPlatformIOS, new GUIContent("iOS"));
+			EditorGUILayout.PropertyField(_propPlatformTvOS, new GUIContent("tvOS"));
+			EditorGUILayout.PropertyField(_propPlatformWindowsUWP, new GUIContent("UWP"));
+			EditorGUILayout.PropertyField(_propPlatformWebGL, new GUIContent("WebGL"));
+			EditorGUILayout.Space();
+			EditorGUILayout.Space();
+			
+			serializedObject.ApplyModifiedProperties();
+
+			bool beginGenerateThumbnails = false;
+			
+			GUILayout.FlexibleSpace();
+			EditorGUI.BeginDisabledGroup(IsGeneratingThumbnails());
+
+			GUILayout.BeginHorizontal();
+			_thumbnailTime = GUILayout.HorizontalSlider(_thumbnailTime, 0f, 1f, GUILayout.ExpandWidth(true));
+			_zoomToFill = GUILayout.Toggle(_zoomToFill, "Zoom And Crop", GUI.skin.button, GUILayout.ExpandWidth(false));
+			GUILayout.EndHorizontal();
+			if (GUILayout.Button("Generate Thumbnail"))
+			{
+				beginGenerateThumbnails = true;
+			}
+			EditorGUI.EndDisabledGroup();
+
+			if (beginGenerateThumbnails)
+			{
+				BeginGenerateThumbnails();
+			}
+
+			if (IsGeneratingThumbnails())
+			{
+				ShowProgress();
+			}
+			if (!IsGeneratingThumbnails())
+			{
+				RemoveProgress();
+			}
+		}
+
+		private void ShowProgress()
+		{
+			// Show cancellable progress
+			float t = (float)_targetIndex / (float)this.targets.Length;
+			t = 0.25f + t * 0.75f;
+			MediaReference media = (this.targets[_targetIndex]) as MediaReference;
+			
+			#if UNITY_2020_1_OR_NEWER
+			if (_progressId < 0)
+			{
+				//Progress.RegisterCancelCallback(_progressId...)
+				_progressId = Progress.Start("[AVProVideo] Generating Thumbnails...", null, Progress.Options.Managed);
+			}
+			Progress.Report(_progressId, t, media.MediaPath.Path);
+			#else
+			if (EditorUtility.DisplayCancelableProgressBar("[AVProVideo] Generating Thumbnails...", media.MediaPath.Path, t))
+			{
+				EndGenerateThumbnails(false);
+			}
+			#endif
+		}
+
+		private void RemoveProgress()
+		{
+			#if UNITY_2020_1_OR_NEWER
+			if (_progressId >= 0)
+			{
+				Progress.Remove(_progressId);
+				_progressId = -1;
+			}
+			#else
+			EditorUtility.ClearProgressBar();
+			#endif
+		}
+
+		#if UNITY_2020_1_OR_NEWER
+		private int _progressId = -1;
+		#endif
+		private float _thumbnailTime;
+		private bool _zoomToFill = false;
+		private int _lastFrame;
+		private BaseMediaPlayer _thumbnailPlayer;
+		private int _mediaFrame = -1;
+		private int _targetIndex = 0;
+		private float _timeoutTimer = 0f;
+
+		private bool IsGeneratingThumbnails()
+		{
+			return (_thumbnailPlayer != null);
+		}
+
+		private void BeginGenerateThumbnails()
+		{
+			EditorApplication.update -= UpdateGenerateThumbnail;
+
+			Debug.Assert(_thumbnailPlayer == null);
+			#if UNITY_EDITOR_WIN
+			if (WindowsMediaPlayer.InitialisePlatform())
+			{
+				MediaPlayer.OptionsWindows options = new MediaPlayer.OptionsWindows();
+				_thumbnailPlayer = new WindowsMediaPlayer(options);
+			}
+			#elif UNITY_EDITOR_OSX
+			{
+				MediaPlayer.OptionsApple options = new MediaPlayer.OptionsApple(MediaPlayer.OptionsApple.TextureFormat.BGRA, MediaPlayer.OptionsApple.Flags.None);
+				_thumbnailPlayer = new AppleMediaPlayer(options);
+			}
+			#endif
+
+			if (_thumbnailPlayer != null)
+			{
+				_targetIndex = 0;
+				BeginNextThumbnail(0);
+			}
+			else
+			{
+				EndGenerateThumbnails(false);
+			}
+		}
+
+		private void BeginNextThumbnail(int index)
+		{
+			EditorApplication.update -= UpdateGenerateThumbnail;
+			_mediaFrame = -1;
+			_timeoutTimer = 0f;
+
+			if (_thumbnailPlayer != null)
+			{
+				if (index < this.targets.Length)
+				{
+					_targetIndex = index;
+					MediaReference media = (this.targets[_targetIndex]) as MediaReference;
+					string path = media.MediaPath.GetResolvedFullPath();
+					bool openedMedia = false;
+					if (!string.IsNullOrEmpty(path))
+					{
+						if (_thumbnailPlayer.OpenMedia(path, 0, string.Empty, media.Hints, 0, false))
+						{
+							openedMedia = true;
+							EditorApplication.update += UpdateGenerateThumbnail;
+						}
+					}
+
+					if (!openedMedia)
+					{
+						// If the media failed to open, continue to the next one
+						BeginNextThumbnail(_targetIndex + 1);
+					}
+				}
+				else
+				{
+					EndGenerateThumbnails(true);
+				}
+			}
+		}
+
+		private void EndGenerateThumbnails(bool updateAssets)
+		{
+			EditorApplication.update -= UpdateGenerateThumbnail;
+			if (_thumbnailPlayer != null)
+			{
+				_thumbnailPlayer.CloseMedia();
+				_thumbnailPlayer.Dispose();
+				_thumbnailPlayer = null;
+			}
+			_mediaFrame = -1;
+
+			if (updateAssets)
+			{
+				// This forces the static preview to refresh
+				foreach (Object o in this.targets)
+				{
+					EditorUtility.SetDirty(o);
+					AssetPreview.GetAssetPreview(o);
+				}
+				AssetDatabase.SaveAssets();
+			}
+		}
+
+		private void UpdateGenerateThumbnail()
+		{
+			if (Time.renderedFrameCount == _lastFrame)
+			{
+				// In at least Unity 5.6 we have to force refresh of the UI otherwise the render thread doesn't run to update the textures
+				this.Repaint();
+				UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
+				return;
+			}
+
+			// Wait for a frame to be rendered
+			Debug.Assert(_thumbnailPlayer != null);
+			if (_thumbnailPlayer != null)
+			{
+				_timeoutTimer += Time.unscaledDeltaTime;
+				bool nextVideo = false;
+				_thumbnailPlayer.Update();
+				_thumbnailPlayer.Render();
+
+				if (_mediaFrame < 0 && _thumbnailPlayer.CanPlay())
+				{
+					_thumbnailPlayer.MuteAudio(true);
+					_thumbnailPlayer.Play();
+					_thumbnailPlayer.Seek(_thumbnailPlayer.GetDuration() * _thumbnailTime);
+					_mediaFrame = _thumbnailPlayer.GetTextureFrameCount();
+				}
+				if (_thumbnailPlayer.GetTexture() != null)
+				{
+					if (_mediaFrame != _thumbnailPlayer.GetTextureFrameCount() && _thumbnailPlayer.GetTextureFrameCount() > 3)
+					{
+						bool prevSRGB = GL.sRGBWrite;
+						GL.sRGBWrite = false;
+
+						RenderTexture rt2 = null;
+						// TODO: move this all into VideoRender as a resolve method
+						{
+							Material materialResolve = new Material(Shader.Find(VideoRender.Shader_Resolve));
+							VideoRender.SetupVerticalFlipMaterial(materialResolve, _thumbnailPlayer.RequiresVerticalFlip());
+							VideoRender.SetupAlphaPackedMaterial(materialResolve, _thumbnailPlayer.GetTextureAlphaPacking());
+							VideoRender.SetupGammaMaterial(materialResolve, !_thumbnailPlayer.PlayerSupportsLinearColorSpace());
+
+							RenderTexture prev = RenderTexture.active;
+
+							// Scale to fit and downsample
+							rt2 = RenderTexture.GetTemporary(128, 128, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
+
+							RenderTexture.active = rt2;
+							GL.Clear(false, true, new Color(0f, 0f, 0f, 0f));
+							ScaleMode scaleMode = ScaleMode.ScaleToFit;
+							if (_zoomToFill)
+							{
+								scaleMode = ScaleMode.ScaleAndCrop;
+							}
+							VideoRender.DrawTexture(new Rect(0f, 0f, 128f, 128f), _thumbnailPlayer.GetTexture(), scaleMode, _thumbnailPlayer.GetTextureAlphaPacking(), _thumbnailPlayer.GetTexturePixelAspectRatio(), materialResolve);
+							RenderTexture.active = prev;
+
+							Material.DestroyImmediate(materialResolve); materialResolve = null;
+						}
+				
+						Texture2D readTexture = new Texture2D(128, 128, TextureFormat.RGBA32, true, false);
+						Helper.GetReadableTexture(rt2, readTexture);
+						MediaReference mediaRef = (this.targets[_targetIndex]) as MediaReference;
+						mediaRef.GeneratePreview(readTexture);
+						DestroyImmediate(readTexture); readTexture = null;
+
+						RenderTexture.ReleaseTemporary(rt2);
+
+						GL.sRGBWrite = prevSRGB;
+						nextVideo = true;
+						Debug.Log("Thumbnail Written");
+					}
+				}
+				if (!nextVideo)
+				{
+					// If there is an error or it times out, then skip this media
+					if (_timeoutTimer > 10f || _thumbnailPlayer.GetLastError() != ErrorCode.None)
+					{
+						MediaReference mediaRef = (this.targets[_targetIndex]) as MediaReference;
+						mediaRef.GeneratePreview(null);
+						nextVideo = true;
+					}
+				}
+
+				if (nextVideo)
+				{
+					BeginNextThumbnail(_targetIndex + 1);
+				}
+			}
+			_lastFrame = Time.renderedFrameCount;
+		}
+
+		public override bool HasPreviewGUI()
+		{
+			return true;
+		}
+
+		public override void OnPreviewGUI(Rect r, GUIStyle background)
+		{
+			Texture texture = RenderStaticPreview(string.Empty, null, 128, 128);
+			if (texture)
+			{
+				GUI.DrawTexture(r, texture, ScaleMode.ScaleToFit, true);
+			}
+		}
+
+		public override Texture2D RenderStaticPreview(string assetPath, Object[] subAssets, int width, int height)
+		{
+			MediaReference media = this.target as MediaReference;
+			if (media)
+			{
+				bool isLinear = false;
+				#if !UNITY_2018_1_OR_NEWER
+				// NOTE: These older versions of Unity don't handle sRGB in the editor correctly so a workaround is to create texture as linear
+				isLinear = true;
+				#endif
+				Texture2D result = new Texture2D(width, height, TextureFormat.RGBA32, true, isLinear);
+				if (!media.GetPreview(result))
+				{
+					DestroyImmediate(result); result = null;
+				}
+				return result;
+			}
+			return null;
+		}
+	}
+}

+ 11 - 0
package/Editor/Scripts/MediaReferenceEditor.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 56fb02c4efc59ef4a8b6328867f1a0bc
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 313 - 0
package/Editor/Scripts/PluginProcessor.cs

@@ -0,0 +1,313 @@
+#if UNITY_2018_1_OR_NEWER || (UNITY_2017_4_OR_NEWER && !UNITY_2017_4_0 && !UNITY_2017_4_1 && !UNITY_2017_4_2 && !UNITY_2017_4_3 && !UNITY_2017_4_4 && !UNITY_2017_4_5 && !UNITY_2017_4_6 && !UNITY_2017_4_7 && !UNITY_2017_4_8 && !UNITY_2017_4_9 && !UNITY_2017_4_10 && !UNITY_2017_4_11 && !UNITY_2017_4_12 && !UNITY_2017_4_13 && !UNITY_2017_4_14 && !UNITY_2017_4_15 && !UNITY_2017_4_15)
+	// Unity added Android ARM64 support in 2018.1, and backported to 2017.4.16
+	#define AVPROVIDEO_UNITY_ANDROID_ARM64_SUPPORT
+#endif
+#if !UNITY_2019_3_OR_NEWER || UNITY_2021_2_OR_NEWER || (UNITY_2020_3_OR_NEWER && !UNITY_2020_3_0 && !UNITY_2020_3_1 && !UNITY_2020_3_2 && !UNITY_2020_3_3 && !UNITY_2020_3_4 && !UNITY_2020_3_5 && !UNITY_2020_3_6 && !UNITY_2020_3_7 && !UNITY_2020_3_8 && !UNITY_2020_3_9 && !UNITY_2020_3_10 && !UNITY_2020_3_11 && !UNITY_2020_3_12 && !UNITY_2020_3_13 && !UNITY_2020_3_14 && !UNITY_2020_3_15 && !UNITY_2020_3_16) || (UNITY_2019_4_OR_NEWER && !UNITY_2019_4_0 && !UNITY_2019_4_1 && !UNITY_2019_4_2 && !UNITY_2019_4_3 && !UNITY_2019_4_4 && !UNITY_2019_4_5 && !UNITY_2019_4_6 && !UNITY_2019_4_7 && !UNITY_2019_4_8 && !UNITY_2019_4_9 && !UNITY_2019_4_10 && !UNITY_2019_4_11 && !UNITY_2019_4_12 && !UNITY_2019_4_13 && !UNITY_2019_4_14 && !UNITY_2019_4_15 && !UNITY_2019_4_16 && !UNITY_2019_4_17 && !UNITY_2019_4_18 && !UNITY_2019_4_19 && !UNITY_2019_4_20 && !UNITY_2019_4_21 && !UNITY_2019_4_22 && !UNITY_2019_4_23 && !UNITY_2019_4_24 && !UNITY_2019_4_25 && !UNITY_2019_4_26 && !UNITY_2019_4_27 && !UNITY_2019_4_28 && !UNITY_2019_4_29 && !UNITY_2019_4_30)
+	// Unity dropped Android x86 support in 2019, but then added it back in 2021.2.0 and backported to 2020.3.17 and 2019.4.31
+	#define AVPROVIDEO_UNITY_ANDROID_X86_SUPPORT
+#endif
+#if UNITY_2021_2_OR_NEWER || (UNITY_2020_3_OR_NEWER && !UNITY_2020_3_0 && !UNITY_2020_3_1 && !UNITY_2020_3_2 && !UNITY_2020_3_3 && !UNITY_2020_3_4 && !UNITY_2020_3_5 && !UNITY_2020_3_6 && !UNITY_2020_3_7 && !UNITY_2020_3_8 && !UNITY_2020_3_9 && !UNITY_2020_3_10 && !UNITY_2020_3_11 && !UNITY_2020_3_12 && !UNITY_2020_3_13 && !UNITY_2020_3_14 && !UNITY_2020_3_15 && !UNITY_2020_3_16) || (UNITY_2019_4_OR_NEWER && !UNITY_2019_4_0 && !UNITY_2019_4_1 && !UNITY_2019_4_2 && !UNITY_2019_4_3 && !UNITY_2019_4_4 && !UNITY_2019_4_5 && !UNITY_2019_4_6 && !UNITY_2019_4_7 && !UNITY_2019_4_8 && !UNITY_2019_4_9 && !UNITY_2019_4_10 && !UNITY_2019_4_11 && !UNITY_2019_4_12 && !UNITY_2019_4_13 && !UNITY_2019_4_14 && !UNITY_2019_4_15 && !UNITY_2019_4_16 && !UNITY_2019_4_17 && !UNITY_2019_4_18 && !UNITY_2019_4_19 && !UNITY_2019_4_20 && !UNITY_2019_4_21 && !UNITY_2019_4_22 && !UNITY_2019_4_23 && !UNITY_2019_4_24 && !UNITY_2019_4_25 && !UNITY_2019_4_26 && !UNITY_2019_4_27 && !UNITY_2019_4_28 && !UNITY_2019_4_29 && !UNITY_2019_4_30)
+	// Unity added Android x86_64 support in 2021.2.0 and backported to 2020.3.17 and 2019.4.31
+	#define AVPROVIDEO_UNITY_ANDROID_X8664_SUPPORT
+#endif
+#if UNITY_2019_1_OR_NEWER
+	#define AVPROVIDEO_UNITY_UWP_ARM64_SUPPORT
+#endif
+#if UNITY_2018_1_OR_NEWER
+	#define AVPROVIDEO_UNITY_BUILDWITHREPORT_SUPPORT
+#endif
+
+using UnityEngine;
+using UnityEditor;
+using UnityEditor.Build;
+#if AVPROVIDEO_UNITY_BUILDWITHREPORT_SUPPORT
+using UnityEditor.Build.Reporting;
+#endif
+using System.Collections.Generic;
+using System.IO;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// Some versions of Unity do not support specific CPU architectures for plugin files
+	/// so this Build Preprocessor checks the plugin files for those and either disables
+	/// them if their arch is not supported, or assigns the correct arch and enables them
+	/// </summary>
+	public class PluginProcessor : 
+	#if AVPROVIDEO_UNITY_BUILDWITHREPORT_SUPPORT
+		IPreprocessBuildWithReport
+	#else
+		IPreprocessBuild
+	#endif
+	{
+		internal class CpuArchitecture
+		{
+			internal CpuArchitecture(string code, bool isSupportedByThisUnityVersion)
+			{
+				_code = code;
+				_isSupportedByThisUnityVersion = isSupportedByThisUnityVersion;
+			}
+			private string _code;
+			private bool _isSupportedByThisUnityVersion;
+
+			internal string Code()
+			{
+				return _code;
+			}
+
+			internal bool IsSupportedByThisUnityVersion()
+			{
+				return _isSupportedByThisUnityVersion;
+			}
+		}
+		
+		internal class PluginFile
+		{
+			internal PluginFile(BuildTarget buildTarget, string relativeFilePath, bool supportsEditor, CpuArchitecture cpuArchitecture)
+			{
+				_buildTarget = buildTarget;
+				_relativeFilePath = relativeFilePath;
+				_cpuArchitecture = cpuArchitecture;
+				_supportsEditor = supportsEditor;
+			}
+
+			internal bool IsBuildTarget(BuildTarget buildTarget)
+			{
+				return (_buildTarget == buildTarget);
+			}
+
+			internal BuildTarget BuildTarget()
+			{
+				return _buildTarget;
+			}
+
+			internal bool IsForFile(string path)
+			{
+				return path.Replace("\\", "/").Contains(_relativeFilePath);
+			}
+
+			internal bool IsSupportedByThisUnityVersion()
+			{
+				return _cpuArchitecture.IsSupportedByThisUnityVersion();
+			}
+
+			internal string CpuArchitectureCode()
+			{
+				return _cpuArchitecture.Code();
+			}
+
+			internal bool SupportsEditor()
+			{
+				return _supportsEditor;
+			}
+
+			private BuildTarget _buildTarget;
+			private string _relativeFilePath;
+			private CpuArchitecture _cpuArchitecture;
+			private bool _supportsEditor;
+		}
+
+		private static List<PluginFile> _pluginFiles = new List<PluginFile>(32);
+
+		internal static void AddPluginFiles(BuildTarget buildTarget, string[] filenames, string folderPrefix, bool supportsEditor, CpuArchitecture cpuArchitecture)
+		{
+			foreach (string filename in filenames)
+			{
+				_pluginFiles.Add(new PluginFile(buildTarget, folderPrefix + filename, supportsEditor, cpuArchitecture));
+			}
+		}
+
+		internal static void AddPlugins_Android()
+		{
+			#if AVPROVIDEO_UNITY_ANDROID_ARM64_SUPPORT
+			const bool IsAndroidArm64Supported = true;
+			#else
+			const bool IsAndroidArm64Supported = false;
+			#endif
+			#if AVPROVIDEO_UNITY_ANDROID_X86_SUPPORT
+			const bool IsAndroidX86Supported = true;
+			#else
+			const bool IsAndroidX86Supported = false;
+			#endif
+			#if AVPROVIDEO_UNITY_ANDROID_X8664_SUPPORT
+			const bool IsAndroidX8664Supported = true;
+			#else
+			const bool IsAndroidX8664Supported = false;
+			#endif
+			string[] filenames = {
+				"libAudio360.so",
+				"libAudio360-JNI.so",
+				"libAVProVideo2Native.so",
+				"libopus.so",
+				"libopusJNI.so",
+				"libresample-rh.so",
+				"libsamplerate-android.so",
+				"libssrc-android.so",
+			};
+			BuildTarget target = BuildTarget.Android;
+			AddPluginFiles(target, filenames, "Android/libs/armeabi-v7a/", false, new CpuArchitecture("ARMv7", true));
+			AddPluginFiles(target, filenames, "Android/libs/arm64-v8a/", false, new CpuArchitecture("ARM64", IsAndroidArm64Supported));
+			AddPluginFiles(target, filenames, "Android/libs/x86/", false, new CpuArchitecture("X86", IsAndroidX86Supported));
+			AddPluginFiles(target, filenames, "Android/libs/x86_64/", false, new CpuArchitecture("X86_64", IsAndroidX8664Supported));
+		}
+
+		internal static void AddPlugins_UWP()
+		{
+			#if AVPROVIDEO_UNITY_UWP_ARM64_SUPPORT
+			const bool IsUwpArm64Supported = true;
+			#else
+			const bool IsUwpArm64Supported = false;
+			#endif
+
+			string[] filenames = {
+				"Audio360.dll",
+				"AVProVideo.dll",
+				"AVProVideoWinRT.dll",
+			};
+			BuildTarget target = BuildTarget.WSAPlayer;
+			AddPluginFiles(target, filenames, "WSA/UWP/ARM/", false, new CpuArchitecture("ARM", true));
+			AddPluginFiles(target, filenames, "WSA/UWP/ARM64/", false, new CpuArchitecture("ARM64", IsUwpArm64Supported));
+			AddPluginFiles(target, filenames, "WSA/UWP/x86/", false, new CpuArchitecture("X86", true));
+			AddPluginFiles(target, filenames, "WSA/UWP/x86_64/", false, new CpuArchitecture("X64", true));
+		}
+
+		private static void BuildPluginFileList()
+		{
+			_pluginFiles.Clear();
+			AddPlugins_Android();
+			AddPlugins_UWP();
+		}
+
+        private class SFileToDelete
+        {
+            public SFileToDelete(string fn)
+            {
+                filename = fn;
+                fullPath = "";
+                found = false;
+            }
+
+            public string filename;
+            public string fullPath;
+            public bool found;
+        };
+
+        private static void RemoveLegacyPluginFiles()
+        {
+            List<SFileToDelete> aFilesToDelete = new List<SFileToDelete>();
+
+#if (UNITY_EDITOR && UNITY_ANDROID)
+            aFilesToDelete.Add( new SFileToDelete( "Android/guava-27.1-android.jar" ) );
+#endif
+
+            if ( aFilesToDelete.Count > 0 )
+            {
+                int iNumFoundFilesToDelete = 0;
+                string aFilesToDeleteString = "";
+
+                PluginImporter[] importers = PluginImporter.GetAllImporters();
+                foreach (PluginImporter pi in importers)
+                {
+                    foreach( SFileToDelete fileToDelete in aFilesToDelete )
+                    {
+                        string pluginFilename = pi.assetPath;
+                        pluginFilename.Replace("\\", "/");
+                        if( pluginFilename.Contains( fileToDelete.filename ) )
+                        {
+                            fileToDelete.fullPath = pi.assetPath;
+                            fileToDelete.found = true;
+
+                            if( iNumFoundFilesToDelete > 0 )
+                            {
+                                aFilesToDeleteString += "\n";
+                            }
+                            aFilesToDeleteString += pi.assetPath;
+                            ++iNumFoundFilesToDelete;
+                        }
+                    }
+                }
+
+                if( iNumFoundFilesToDelete > 0 )
+                {
+                    string message = ( iNumFoundFilesToDelete == 1 ) ? "A legacy AVPro Video plugin file has been found that requires deleting in order to build." : "Legacy AVPro Video plugin files have been found that require deleting in order to build.";
+                    Debug.Log("[AVProVideo] " + message + " Files: " + aFilesToDeleteString );
+                    if ( EditorUtility.DisplayDialog( "AVPro Video Legacy File", message + "\n\nDelete the following files?\n\n" + aFilesToDeleteString, "Delete", "Ignore" ) )
+                    {
+                        foreach( SFileToDelete fileToDelete in aFilesToDelete )
+                        {
+                            bool bDeleted = AssetDatabase.DeleteAsset( fileToDelete.fullPath );
+                            if( bDeleted )
+                            {
+                                Debug.Log( "[AVProVideo] Deleting " + fileToDelete.fullPath );
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        public int callbackOrder { get { return 0; } }
+
+#if AVPROVIDEO_UNITY_BUILDWITHREPORT_SUPPORT
+		public void OnPreprocessBuild(BuildReport report)
+		{
+            RemoveLegacyPluginFiles();
+
+            BuildPluginFileList();
+			CheckNativePlugins(report.summary.platform);
+		}
+#else
+		public void OnPreprocessBuild(BuildTarget target, string path)
+		{
+            RemoveLegacyPluginFiles();
+
+			BuildPluginFileList();
+			CheckNativePlugins(target);
+		}
+#endif
+
+		internal static void CheckNativePlugins(BuildTarget target)
+		{
+			PluginImporter[] importers = PluginImporter.GetAllImporters();
+			foreach (PluginImporter pi in importers)
+			{
+				// Currently we're only interested in native plugins
+				if (!pi.isNativePlugin) continue;
+
+				// Skip plugins that aren't in the AVProVideo path
+				// NOTE: This is commented out for now to allow the case where users have moved the plugin files to another folder.
+				// Eventually might need a more robust method, perhaps using GUIDS
+				//if (!pi.assetPath.Contains("AVProVideo")) continue;
+
+				foreach (PluginFile pluginFile in _pluginFiles)
+				{
+					if (pluginFile.IsBuildTarget(target) && 
+						pluginFile.IsForFile(pi.assetPath))
+					{
+						pi.SetCompatibleWithAnyPlatform(false);
+						if (pluginFile.IsSupportedByThisUnityVersion())
+						{
+							Debug.Log("[AVProVideo] Enabling " + pluginFile.CpuArchitectureCode() + " " + pi.assetPath);
+							pi.SetCompatibleWithEditor(pluginFile.SupportsEditor());
+							pi.SetCompatibleWithPlatform(pluginFile.BuildTarget(), true);
+							pi.SetPlatformData(pluginFile.BuildTarget(), "CPU", pluginFile.CpuArchitectureCode());
+						}
+						else
+						{
+							pi.SetCompatibleWithEditor(false);
+							pi.SetCompatibleWithPlatform(pluginFile.BuildTarget(), false);
+							pi.SetPlatformData(pluginFile.BuildTarget(), "CPU", "");
+							Debug.Log("[AVProVideo] Disabling " + pluginFile.CpuArchitectureCode() + " " + pi.assetPath);
+						}
+						pi.SaveAndReimport();
+						break;
+					}
+				}
+			}
+		}
+	}
+}

+ 12 - 0
package/Editor/Scripts/PluginProcessor.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: e2b91117f576bb5438faa22e38d811b3
+timeCreated: 1448902492
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 33 - 0
package/Editor/Scripts/PostProcessBuild.cs

@@ -0,0 +1,33 @@
+#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
+using System.IO;
+using UnityEngine;
+using UnityEditor;
+using UnityEditor.Callbacks;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	public class PostProcessBuild
+	{
+		[PostProcessBuild]
+ 		public static void OnPostProcessBuild(BuildTarget target, string path)
+ 		{
+			bool x86 = false;
+#if UNITY_2017_3_OR_NEWER
+			// 64-bit only from here on out, woo hoo!!! \o/
+#else
+			x86 = target == BuildTarget.StandaloneOSXIntel || target == BuildTarget.StandaloneOSXUniversal;
+#endif
+			if (x86)
+			{
+				string message = "AVPro Video doesn't support target StandaloneOSXIntel (32-bit), please use StandaloneOSXIntel64 (64-bit) or remove this PostProcessBuild script";
+				Debug.LogError(message);
+				EditorUtility.DisplayDialog("AVPro Video", message, "Ok");
+			}
+		}
+	}
+}
+#endif // UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX

+ 8 - 0
package/Editor/Scripts/PostProcessBuild.cs.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 1e59c97bf0125284dab6c83833518fcf
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 

+ 65 - 0
package/Editor/Scripts/PostProcessBuild_Android.cs

@@ -0,0 +1,65 @@
+#if UNITY_ANDROID
+
+using UnityEngine;
+using UnityEditor.Android;
+using System.IO;
+using System.Text;
+
+//-----------------------------------------------------------------------------
+// Copyright 2012-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	public class PostProcessBuild_Android : IPostGenerateGradleAndroidProject
+	{
+		public int callbackOrder { get { return 1; } }
+
+		public void OnPostGenerateGradleAndroidProject( string path )
+		{
+			GradleProperty( path );
+		}
+
+		private void GradleProperty( string path )
+		{
+#if UNITY_2020_1_OR_NEWER || UNITY_2020_OR_NEWER
+			// When using Unity 2020.1 and above it has been seen that the build process overly optimises which causes issues in the ExoPlayer library.
+			// To overcome this issue, we need to add 'android.enableDexingArtifactTransform=false' to the gradle.properties.
+			// Note that this can be done by the developer at project level already.
+
+			Debug.Log("[AVProVideo] Post-processing Android project: patching gradle.properties");
+
+			StringBuilder stringBuilder = new StringBuilder();
+
+			// Path to gradle.properties
+			string filePath = Path.Combine( path, "..", "gradle.properties" );
+
+			if( File.Exists( filePath ) )
+			{
+				// Load in all the lines in the file
+				string[] allLines = File.ReadAllLines( filePath );
+
+				foreach( string line in allLines )
+				{
+					if( line.Length > 0 )
+					{
+						// Add everything except enableDexingArtifactTransform
+						if ( !line.Contains( "android.enableDexingArtifactTransform" ) )
+						{
+							stringBuilder.AppendLine( line );
+						}
+					}
+				}
+			}
+
+			// Add in line to set enableDexingArtifactTransform to false
+			stringBuilder.AppendLine( "android.enableDexingArtifactTransform=false" );
+
+			// Write out the amended file
+			File.WriteAllText( filePath, stringBuilder.ToString() );
+#endif
+		}
+	}
+}
+
+#endif // UNITY_ANDROID

+ 11 - 0
package/Editor/Scripts/PostProcessBuild_Android.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 532847372d6add8498ce0da18f7a619e
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 270 - 0
package/Editor/Scripts/PostProcessBuild_iOS.cs

@@ -0,0 +1,270 @@
+#if (UNITY_IOS || UNITY_TVOS) && UNITY_2017_1_OR_NEWER
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEditor;
+using UnityEditor.Callbacks;
+using UnityEditor.iOS.Xcode;
+using UnityEditor.iOS.Xcode.Extensions;
+
+//-----------------------------------------------------------------------------
+// Copyright 2012-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	public class PostProcessBuild_iOS
+	{
+		const string PluginName = "AVProVideo.framework";
+
+		// Simple holder for major.minor version number
+		private readonly struct Version
+		{
+			public static Version unknown = new Version(0, 0);
+			public static Version _10_0 = new Version(10, 0);
+			public static Version _12_2 = new Version(12, 2);
+
+			public readonly int major;
+			public readonly int minor;
+
+			public Version(int major, int minor)
+			{
+				this.major = major;
+				this.minor = minor;
+			}
+
+			public static bool operator ==(Version lhs, Version rhs) => (lhs.major == rhs.major && lhs.minor == rhs.minor);
+			public static bool operator !=(Version lhs, Version rhs) => (lhs.major != rhs.major || lhs.minor != rhs.minor);
+			public static bool operator  <(Version lhs, Version rhs) => (lhs.major < rhs.major) || (lhs.major == rhs.major && lhs.minor < rhs.minor);
+			public static bool operator  >(Version lhs, Version rhs) => (lhs.major > rhs.major) || (lhs.major == rhs.major && lhs.minor > rhs.minor);
+
+			public override bool Equals(object obj) { return base.Equals(obj); }
+			public override int GetHashCode() { return base.GetHashCode(); }
+			public override string ToString() { return string.Format("{0}.{1}", major, minor); }
+		}
+
+		private class Platform
+		{
+			public BuildTarget target { get; }
+			public string name { get; }
+			public string guid { get; }
+
+			// Accessor for PlayerSettings.Platform.targetOSVersionString
+			public string targetOSVersionString
+			{
+				get
+				{
+					switch (target)
+					{
+					case BuildTarget.iOS:
+						return PlayerSettings.iOS.targetOSVersionString;
+					case BuildTarget.tvOS:
+						return PlayerSettings.tvOS.targetOSVersionString;
+					default:
+						return null;
+					}
+				}
+			}
+
+			// Will lazily set version from targetOSVersionString when called for the first time
+			private Version _version = Version.unknown;
+			public Version targetOSVersion
+			{
+				get
+				{
+					if (_version == Version.unknown)
+					{
+						if (targetOSVersionString != null)
+						{
+							string[] version = targetOSVersionString.Split('.');
+							if (version != null && version.Length >= 1)
+							{
+								int major = 0;
+								if (int.TryParse(version[0], out major) && version.Length >= 2)
+								{
+									int minor = 0;
+									if (int.TryParse(version[1], out minor))
+									{
+										_version = new Version(major, minor);
+									}
+								}
+							}
+						}
+						if (_version == Version.unknown)
+						{
+							// 10.0 is the minumum version we support so default to this
+							_version = Version._10_0;
+						}
+					}
+					return _version;
+				}
+			}
+
+			public static Platform GetPlatformForTarget(BuildTarget target)
+			{
+				switch (target)
+				{
+					case BuildTarget.iOS:
+						return new Platform(BuildTarget.iOS, "iOS", "2a1facf97326449499b63c03811b1ab2");
+
+					case BuildTarget.tvOS:
+						return new Platform(BuildTarget.tvOS, "tvOS", "bcf659e3a94d748d6a100d5531540d1a");
+
+					default:
+						return null;
+				}
+			}
+
+			private Platform(BuildTarget target, string name, string guid)
+			{
+				this.target = target;
+				this.name = name;
+				this.guid = guid;
+			}
+		}
+
+		private static string PluginPathForPlatform(Platform platform)
+		{
+			// See if we can find the plugin by GUID
+			string pluginPath = AssetDatabase.GUIDToAssetPath(platform.guid);
+
+			// If not, try and find it by name
+			if (pluginPath.Length == 0)
+			{
+				Debug.LogWarningFormat("[AVProVideo] Failed to find plugin by GUID, will attempt to find it by name.");
+				string[] guids = AssetDatabase.FindAssets(PluginName);
+				if (guids != null && guids.Length > 0)
+				{
+					foreach (string guid in guids)
+					{
+						string assetPath = AssetDatabase.GUIDToAssetPath(guid);
+						if (assetPath.Contains(platform.name))
+						{
+							pluginPath = assetPath;
+							break;
+						}
+					}
+				}
+			}
+
+			if (pluginPath.Length > 0)
+			{
+				Debug.LogFormat("[AVProVideo] Found plugin at '{0}'", pluginPath);
+			}
+
+			return pluginPath;
+		}
+
+		// Converts the Unity asset path to the expected path in the built Xcode project.
+		private static string ConvertPluginAssetPathToXcodeProjectFrameworkPath(string pluginPath)
+		{
+			List<string> components = new List<string>(pluginPath.Split(new char[] { '/' }));
+			components[0] = "Frameworks";
+#if UNITY_2019_1_OR_NEWER
+				string frameworkPath = string.Join("/", components);
+#else
+				string frameworkPath = string.Join("/", components.ToArray());
+#endif
+			return frameworkPath;
+		}
+
+		// Helper to set the file execute bits
+		private static void SetFileExecutePermission(string path)
+		{
+#if UNITY_EDITOR_OSX
+			Debug.LogFormat("[AVProVideo] Checking permissions on {0}", path);
+
+			string cmd = string.Format("if [ ! -x \"{0}\" ]; then echo \"Missing execute permissions, fixing...\"; chmod a+x \"{0}\"; else echo \"All good\"; fi", path);
+			string args = string.Format("-c \"{0}\"", cmd.Replace("\"", "\\\""));
+
+			System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo("/bin/sh");
+			startInfo.Arguments = args;
+			startInfo.RedirectStandardOutput = true;
+			startInfo.RedirectStandardError = true;
+			startInfo.UseShellExecute = false;
+			startInfo.CreateNoWindow = true;
+
+			System.Diagnostics.Process process = System.Diagnostics.Process.Start(startInfo);
+			string result = process.StandardOutput.ReadToEnd();
+			string error = process.StandardError.ReadToEnd();
+			process.WaitForExit();
+
+			if (error != null && error.Length > 0)
+			{
+				Debug.LogErrorFormat("[AVProVideo] Failed to set execute permissions on the plugin binary, error: {0}", error);
+			}
+			else if (result != null && result.Length > 0)
+			{
+				Debug.LogFormat("[AVProVideo] {0}", result);
+			}
+#else
+			Debug.LogWarningFormat("[AVProVideo] Project is not being built on macOS so we are unable to check the file permissions on the plugin binary. You need to make sure that the execute bits are set on \"AVProVideo.framework/AVProVideo\" before building the Xcode project.");
+#endif
+		}
+
+		[PostProcessBuild]
+		public static void ModifyProject(BuildTarget target, string path)
+		{
+			if (target != BuildTarget.iOS && target != BuildTarget.tvOS)
+				return;
+
+			Debug.Log("[AVProVideo] Post-processing Xcode project.");
+			Platform platform = Platform.GetPlatformForTarget(target);
+			if (platform == null)
+			{
+				Debug.LogWarningFormat("[AVProVideo] Unknown build target: {0}", target.ToString());
+				return;
+			}
+
+			string projectPath = path + "/Unity-iPhone.xcodeproj/project.pbxproj";
+			PBXProject project = new PBXProject();
+			project.ReadFromFile(projectPath);
+
+			// Attempt to find the plugin path
+			string pluginPath = PluginPathForPlatform(platform);
+			if (pluginPath.Length > 0)
+			{
+#if UNITY_2019_3_OR_NEWER
+					string targetGuid = project.GetUnityMainTargetGuid();
+#else
+					string targetGuid = project.TargetGuidByName(PBXProject.GetUnityTargetName());
+#endif
+
+				string frameworkPath = ConvertPluginAssetPathToXcodeProjectFrameworkPath(pluginPath);
+				string fileGuid = project.FindFileGuidByProjectPath(frameworkPath);
+				if (fileGuid != null)
+				{
+					// Make sure the plugin binary has execute permissions set.
+					// For reasons unknown these are being lost somewhere between the plugin package being built and imported from the asset store.
+					string binaryPath = System.IO.Path.Combine(path, frameworkPath, "AVProVideo");
+					SetFileExecutePermission(binaryPath);
+
+					Debug.LogFormat("[AVProVideo] Adding 'AVProVideo.framework' to the list of embedded frameworks");
+					PBXProjectExtensions.AddFileToEmbedFrameworks(project, targetGuid, fileGuid);
+
+					Debug.LogFormat("[AVProVideo] Setting 'LD_RUNPATH_SEARCH_PATHS' to '$(inherited) @executable_path/Frameworks'");
+					project.SetBuildProperty(targetGuid, "LD_RUNPATH_SEARCH_PATHS", "$(inherited) @executable_path/Frameworks");
+				}
+				else
+				{
+					Debug.LogWarningFormat("[AVProVideo] Failed to find {0} in the generated project. You will need to manually set {0} to 'Embed & Sign' in the Xcode project's framework list.", PluginName);
+				}
+
+				// See if we need to enable embedding of Swift binaries
+				if (platform.targetOSVersion < Version._12_2)
+				{
+					Debug.LogFormat("[AVProVideo] Target OS version '{0}' is < 12.2, setting 'ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES' to 'YES'", platform.targetOSVersion);
+					project.SetBuildProperty(targetGuid, "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES", "YES");
+				}
+
+				Debug.LogFormat("[AVProVideo] Writing out Xcode project file");
+				project.WriteToFile(projectPath);
+			}
+			else
+			{
+				Debug.LogErrorFormat("Failed to find '{0}' for '{1}' in the Unity project. Something is horribly wrong, please reinstall AVPro Video.", PluginName, platform);
+			}
+		}
+	}
+}
+
+#endif

+ 12 - 0
package/Editor/Scripts/PostProcessBuild_iOS.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 65f3d7a146fd284418d5a70fd677b7b2
+timeCreated: 1591790256
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 170 - 0
package/Editor/Scripts/PostProcessBuild_macOS.cs

@@ -0,0 +1,170 @@
+#if UNITY_EDITOR && UNITY_2019_1_OR_NEWER
+
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using UnityEngine;
+using UnityEditor;
+using UnityEditor.Callbacks;
+
+//-----------------------------------------------------------------------------
+// Copyright 2012-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	public class PBXProjectHandlerException : System.Exception
+	{
+		public PBXProjectHandlerException(string message)
+		:	base(message)
+		{
+
+		}
+	}
+
+	public class PBXProjectHandler
+	{
+		private static System.Type _PBXProjectType;
+		private static System.Type PBXProjectType
+		{
+			get
+			{
+				if (_PBXProjectType == null)
+				{
+					_PBXProjectType = System.Type.GetType("UnityEditor.iOS.Xcode.PBXProject, UnityEditor.iOS.Extensions.Xcode, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null");
+					if (_PBXProjectType == null)
+					{
+						throw new PBXProjectHandlerException("Failed to get type \"PBXProject\"");
+					}
+				}
+				return _PBXProjectType;
+			}
+		}
+
+		private static Dictionary<string, MethodInfo> _PBXProjectTypeMethods;
+		private static Dictionary<string, MethodInfo> PBXProjectTypeMethods
+		{
+			get
+			{
+				if (_PBXProjectTypeMethods == null)
+				{
+					_PBXProjectTypeMethods = new Dictionary<string, MethodInfo>();
+				}
+				return _PBXProjectTypeMethods;
+			}
+		}
+
+		private static MethodInfo GetMethod(string name, System.Type[] types)
+		{
+			string lookup = name + types.ToString();
+			MethodInfo method;
+			if (!PBXProjectTypeMethods.TryGetValue(lookup, out method))
+			{
+				method = _PBXProjectType.GetMethod(name, types);
+				if (method != null)
+				{
+					_PBXProjectTypeMethods[lookup] = method;
+				}
+				else
+				{
+					throw new PBXProjectHandlerException(string.Format("Unknown method \"{0}\"", name));
+				}
+			}
+			return method;
+		}
+
+		private object _project;
+
+		public PBXProjectHandler()
+		{
+			_project = System.Activator.CreateInstance(PBXProjectType);
+		}
+
+		public void ReadFromFile(string path)
+		{
+			MethodInfo method = GetMethod("ReadFromFile", new System.Type[] { typeof(string) });
+			Debug.LogFormat("[AVProVideo] Reading Xcode project at: {0}", path);
+			method.Invoke(_project, new object[] { path });
+		}
+
+		public void WriteToFile(string path)
+		{
+			MethodInfo method = GetMethod("WriteToFile", new System.Type[] { typeof(string) });
+			Debug.LogFormat("[AVProVideo] Writing Xcode project to: {0}", path);
+			method.Invoke(_project, new object[] { path });
+		}
+
+		public string TargetGuidByName(string name)
+		{
+			MethodInfo method = GetMethod("TargetGuidByName", new System.Type[] { typeof(string) });
+			string guid = (string)method.Invoke(_project, new object[] { name });
+			Debug.LogFormat("[AVProVideo] Target GUID for '{0}' is '{1}'", name, guid);
+			return guid;
+		}
+
+		public void SetBuildProperty(string guid, string property, string value)
+		{
+			MethodInfo method = GetMethod("SetBuildProperty", new System.Type[] { typeof(string), typeof(string), typeof(string) });
+			Debug.LogFormat("[AVProVideo] Setting build property '{0}' to '{1}' for target with guid '{2}'", property, value, guid);
+			method.Invoke(_project, new object[] { guid, property, value });
+		}
+	}
+
+	public class PostProcessBuild_macOS
+	{
+		private static bool ActualModifyProjectAtPath(string path)
+		{
+			if (!Directory.Exists(path))
+			{
+				Debug.LogWarningFormat("[AVProVideo] Failed to find Xcode project with path: {0}", path);
+				return false;
+			}
+
+			Debug.LogFormat("[AVProVideo] Modifying Xcode project at: {0}", path);
+			string projectPath = Path.Combine(path, "project.pbxproj");
+			try
+			{
+				PBXProjectHandler handler = new PBXProjectHandler();
+				handler.ReadFromFile(projectPath);
+				string guid = handler.TargetGuidByName(Application.productName);
+				handler.SetBuildProperty(guid, "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES", "YES");
+				handler.WriteToFile(projectPath);
+				return true;
+			}
+			catch (PBXProjectHandlerException ex)
+			{
+				Debug.LogErrorFormat("[AVProVideo] {0}", ex);
+			}
+
+			return false;
+		}
+
+		[PostProcessBuild]
+		public static void ModifyProject(BuildTarget target, string path)
+		{
+			if (target != BuildTarget.StandaloneOSX)
+				return;
+
+#if AVPROVIDEO_SUPPORT_MACOSX_10_14_3_AND_OLDER
+
+			Debug.Log("[AVProVideo] Post-processing Xcode project");
+
+			string projectPath = Path.Combine(path, Path.GetFileName(path) + ".xcodeproj");
+			if (ActualModifyProjectAtPath(projectPath))
+			{
+				Debug.Log("[AVProVideo] Finished");
+			}
+			else
+			{
+				Debug.LogError("[AVProVideo] Failed to modify Xcode project");
+				Debug.Log("[AVProVideo] You will need to manually set \"Always Embed Swift Standard Libraries\" to \"YES\" in the target's build settings if you're targetting macOS versions prior to 10.14.4");
+			}
+
+#endif // AVPROVIDEO_SUPPORT_MACOSX_10_14_3_AND_OLDER
+
+		}
+	}
+
+}   // namespace RenderHeads.Media.AVProVideo.Editor
+
+#endif

+ 11 - 0
package/Editor/Scripts/PostProcessBuild_macOS.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b5e7d0eecf59540cca67bfad3855cef1
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 121 - 0
package/Editor/Scripts/PreProcessBuild.cs

@@ -0,0 +1,121 @@
+#if UNITY_2018_1_OR_NEWER
+	#define UNITY_SUPPORTS_BUILD_REPORT
+#endif
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEditor;
+using UnityEngine.Rendering;
+using UnityEditor.Build;
+#if UNITY_SUPPORTS_BUILD_REPORT
+using UnityEditor.Build.Reporting;
+#endif
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	public class PreProcessBuild : 
+		#if UNITY_SUPPORTS_BUILD_REPORT
+		IPreprocessBuildWithReport
+		#else
+		IPreprocessBuild
+		#endif
+	{
+		public int callbackOrder { get { return 0; } }
+
+	#if UNITY_SUPPORTS_BUILD_REPORT
+		public void OnPreprocessBuild(BuildReport report)
+		{
+			OnPreprocessBuild(report.summary.platform, report.summary.outputPath);
+		}
+	#endif
+
+		public void OnPreprocessBuild(BuildTarget target, string path)
+		{
+			if (IsTargetMacOS(target) || target == BuildTarget.iOS || target == BuildTarget.tvOS)
+			{
+				int indexMetal = GetGraphicsApiIndex(target, GraphicsDeviceType.Metal);
+				int indexOpenGLCore = GetGraphicsApiIndex(target, GraphicsDeviceType.OpenGLCore);
+				int indexOpenGLES2 = GetGraphicsApiIndex(target, GraphicsDeviceType.OpenGLES2);
+				int indexOpenGLES3 = GetGraphicsApiIndex(target, GraphicsDeviceType.OpenGLES3);
+
+				if (indexMetal < 0)
+				{
+					string message = "Metal graphics API is required by AVPro Video.";
+					message += "\n\nPlease go to Player Settings > Auto Graphics API and add Metal to the top of the list.";
+					ShowAbortDialog(message);
+				}
+
+				if (indexOpenGLCore >= 0 && indexMetal >=0 && indexOpenGLCore < indexMetal)
+				{
+					string message = "OpenGL graphics API is not supported by AVPro Video.";
+					message += "\n\nVideo will play but no video frames will be displayed.";
+					message += "\n\nPlease go to Player Settings > Auto Graphics API and add Metal to the top of the list.";
+					ShowAbortDialog(message);
+				}
+
+				if (indexOpenGLES2 >= 0 && indexMetal >=0 && indexOpenGLES2 < indexMetal)
+				{
+					string message = "OpenGLES2 graphics API is not supported by AVPro Video.";
+					message += "\n\nVideo will play but no video frames will be displayed.";
+					message += "\n\nPlease go to Player Settings > Auto Graphics API and add Metal to the top of the list.";
+					ShowAbortDialog(message);
+				}
+
+				if (indexOpenGLES3 >= 0 && indexMetal >=0 && indexOpenGLES3 < indexMetal)
+				{
+					string message = "OpenGLES3 graphics API is not supported by AVPro Video.";
+					message += "\n\nVideo will play but no video frames will be displayed.";
+					message += "\n\nPlease go to Player Settings > Auto Graphics API and add Metal to the top of the list.";
+					ShowAbortDialog(message);
+				}
+			}
+
+			int indexVulkan = GetGraphicsApiIndex(target, GraphicsDeviceType.Vulkan);
+			if (indexVulkan >= 0)
+			{
+				string message = "Vulkan graphics API is not supported by AVPro Video.";
+				if (target == BuildTarget.Android)
+				{
+					message += "\n\nPlease go to Player Settings > Android > Auto Graphics API and remove Vulkan from the list.\nOnly OpenGL ES 2.0 and 3.0 are supported on Android.";
+				}
+				else
+				{
+					message += "\n\nPlease go to Player Settings > Auto Graphics API and remove Vulkan from the list.";
+				}
+				ShowAbortDialog(message);
+			}
+		}
+
+		static void ShowAbortDialog(string message)
+		{
+			if (!EditorUtility.DisplayDialog("Continue Build?", message, "Continue", "Cancel"))
+			{
+				throw new BuildFailedException(message);
+			}
+		}
+
+		static bool IsTargetMacOS(BuildTarget target)
+		{
+			#if UNITY_2017_3_OR_NEWER
+			return (target == BuildTarget.StandaloneOSX);
+			#else
+			return (target == BuildTarget.StandaloneOSXUniversal || target == BuildTarget.StandaloneOSXIntel);
+			#endif
+		}
+
+		static int GetGraphicsApiIndex(BuildTarget target, GraphicsDeviceType api)
+		{
+			int result = -1;
+			GraphicsDeviceType[] devices = UnityEditor.PlayerSettings.GetGraphicsAPIs(target);
+			for (int i = 0; i < devices.Length; i++)
+			{
+				if (devices[i] == api)
+				{
+					result = i;
+					break;
+				}
+			}
+			return result;
+		}
+	}
+}

+ 12 - 0
package/Editor/Scripts/PreProcessBuild.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 310970e3e18699c43bbab984cf33049e
+timeCreated: 1620956493
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 91 - 0
package/Editor/Scripts/RecentItems.cs

@@ -0,0 +1,91 @@
+using UnityEngine;
+using UnityEditor;
+using System.Collections.Generic;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// </summary>
+	public static class RecentItems
+	{
+		private const int MaxRecentItems = 16;
+
+		private static List<string> _recentFiles = new List<string>(MaxRecentItems);
+		private static List<string> _recentUrls = new List<string>(MaxRecentItems);
+		// TODO: add a list for favourites to allow user to create their own list?
+
+		public static List<string> Files { get { return _recentFiles; } }
+		public static List<string> Urls { get { return _recentUrls; } }
+
+		static RecentItems()
+		{
+			MediaPlayer.InternalMediaLoadedEvent.RemoveListener(Add);
+			MediaPlayer.InternalMediaLoadedEvent.AddListener(Add);
+		}
+
+		public static void Load()
+		{
+			_recentFiles = EditorHelper.GetEditorPrefsToStringList(MediaPlayerEditor.SettingsPrefix + "RecentFiles");
+			_recentUrls = EditorHelper.GetEditorPrefsToStringList(MediaPlayerEditor.SettingsPrefix + "RecentUrls");
+		}
+
+		public static void Save()
+		{
+			EditorHelper.SetEditorPrefsFromStringList(MediaPlayerEditor.SettingsPrefix + "RecentFiles", _recentFiles);
+			EditorHelper.SetEditorPrefsFromStringList(MediaPlayerEditor.SettingsPrefix + "RecentUrls", _recentUrls);
+		}
+
+		public static void Add(string path)
+		{
+			if (path.Contains("://"))
+			{
+				Add(path, _recentUrls);
+			}
+			else
+			{
+				Add(path, _recentFiles);
+			}
+		}
+
+		private static void Add(string path, List<string> list)
+		{
+			if (!list.Contains(path))
+			{
+				list.Insert(0, path);
+				if (list.Count > MaxRecentItems)
+				{
+					// Remove the oldest item from the list
+					list.RemoveAt(list.Count - 1);
+				}
+			}
+			else
+			{
+				// If it already contains the item, then move it to the top
+				list.Remove(path);
+				list.Insert(0, path);
+			}
+			Save();
+		}
+
+		public static void ClearMissingFiles()
+		{
+			if (_recentFiles != null && _recentFiles.Count > 0)
+			{
+				List<string> newList = new List<string>(_recentFiles.Count);
+				for (int i = 0; i < _recentFiles.Count; i++)
+				{
+					string path = _recentFiles[i];
+					if (System.IO.File.Exists(path))
+					{
+						newList.Add(path);
+					}
+				}
+				_recentFiles = newList;
+			}
+		}
+	}
+}

+ 12 - 0
package/Editor/Scripts/RecentItems.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 397a9516709d2504c85543618f07bff3
+timeCreated: 1448902492
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 289 - 0
package/Editor/Scripts/RecentMenu.cs

@@ -0,0 +1,289 @@
+using UnityEngine;
+using UnityEditor;
+using System.Collections.Generic;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// </summary>
+	public class RecentMenu
+	{
+		public static void Create(SerializedProperty propPath, SerializedProperty propMediaSource, string fileExtensions, bool autoLoadMedia, int mediaReferencePickerId = -1)
+		{
+			RecentItems.Load();
+			RecentMenu menu = new RecentMenu();
+			menu.FileBrowseButton(propPath, propMediaSource, fileExtensions, autoLoadMedia, mediaReferencePickerId);
+		}
+
+		private void FileBrowseButton(SerializedProperty propPath, SerializedProperty propMediaSource, string fileExtensions, bool autoLoadMedia, int mediaReferencePickerId = -1)
+		{
+			GenericMenu toolsMenu = new GenericMenu();
+			if (mediaReferencePickerId >= 0)
+			{
+				toolsMenu.AddItem(new GUIContent("Media References..."), false, Callback_BrowseMediaReferences, (object)mediaReferencePickerId);
+			}
+			toolsMenu.AddItem(new GUIContent("Browse..."), false, Callback_Browse, new BrowseData(propPath, propMediaSource, fileExtensions, autoLoadMedia));
+			CreateMenu_StreamingAssets(toolsMenu, "StreamingAssets/", propPath, propMediaSource, autoLoadMedia);
+			CreateMenu_RecentFiles(toolsMenu, RecentItems.Files, "Recent Files/" , propPath, propMediaSource, autoLoadMedia);
+			CreateMenu_RecentUrls(toolsMenu, RecentItems.Urls, "Recent URLs/", propPath, propMediaSource, autoLoadMedia);
+			toolsMenu.ShowAsContext();
+		}
+
+		private struct RecentMenuItemData
+		{
+			public RecentMenuItemData(string path, SerializedProperty propPath, SerializedProperty propMediaSource, bool autoLoadMedia)
+			{
+				this.path = path;
+				this.propPath = propPath;
+				this.propMediaSource = propMediaSource;
+				this.autoLoadMedia = autoLoadMedia;
+			}
+
+			public string path;
+			public bool autoLoadMedia;
+			public SerializedProperty propPath;
+			public SerializedProperty propMediaSource;
+		}
+
+		private void Callback_Select(object obj)
+		{
+			RecentMenuItemData data = (RecentMenuItemData)obj;
+
+			// Move it to the top of the list
+			RecentItems.Add(data.path);
+
+			// Resolve to relative path
+			MediaPath mediaPath = EditorHelper.GetMediaPathFromFullPath(data.path);
+
+			SerializedProperty propMediaPath = data.propPath.FindPropertyRelative("_path");
+			SerializedProperty propMediaPathType = data.propPath.FindPropertyRelative("_pathType");
+
+			// Assign to properties
+			propMediaPath.stringValue = mediaPath.Path.Replace("\\", "/");
+			propMediaPathType.enumValueIndex = (int)mediaPath.PathType;
+			if (data.propMediaSource != null) data.propMediaSource.enumValueIndex = (int)MediaSource.Path;
+
+			// Mark as modified
+			data.propPath.serializedObject.ApplyModifiedProperties();
+			foreach (Object o in data.propPath.serializedObject.targetObjects)
+			{
+				EditorUtility.SetDirty(o);
+			}
+
+			if (data.autoLoadMedia)
+			{
+				MediaPlayer mediaPlayer = (MediaPlayer)data.propPath.serializedObject.targetObject;
+				if (mediaPlayer != null)
+				{
+					mediaPlayer.OpenMedia(mediaPlayer.MediaPath, autoPlay:true);
+				}
+			}
+		}
+
+		private void Callback_ClearList(object obj)
+		{
+			((List<string>)obj).Clear();
+			RecentItems.Save();
+		}
+
+		private void Callback_ClearMissingFiles()
+		{
+			RecentItems.ClearMissingFiles();
+			RecentItems.Save();
+		}
+
+		private struct BrowseData
+		{
+			public BrowseData(SerializedProperty propPath, SerializedProperty propMediaSource, string extensions, bool autoLoadMedia)
+			{
+				this.extensions = extensions;
+				this.propPath = propPath;
+				this.propMediaSource = propMediaSource;
+				this.autoLoadMedia = autoLoadMedia;
+			}
+
+			public bool autoLoadMedia;
+			public string extensions;
+			public SerializedProperty propPath;
+			public SerializedProperty propMediaSource;
+		}
+
+		private void Callback_BrowseMediaReferences(object obj)
+		{
+			int controlID = (int)obj;
+			EditorGUIUtility.ShowObjectPicker<MediaReference>(null, false, "", controlID);
+		}
+
+		private void Callback_Browse(object obj)
+		{
+			BrowseData data = (BrowseData)obj;
+			SerializedProperty propFilePath = data.propPath.FindPropertyRelative("_path");
+			SerializedProperty propFilePathType = data.propPath.FindPropertyRelative("_pathType");
+			string startFolder = EditorHelper.GetBrowsableFolder(propFilePath.stringValue, (MediaPathType)propFilePathType.enumValueIndex);
+			string videoPath = propFilePath.stringValue;
+			string fullPath = string.Empty;
+			MediaPath mediaPath = new MediaPath();
+			if (EditorHelper.OpenMediaFileDialog(startFolder, ref mediaPath, ref fullPath, data.extensions))
+			{
+				// Assign to properties
+				propFilePath.stringValue = mediaPath.Path.Replace("\\", "/");
+				propFilePathType.enumValueIndex = (int)mediaPath.PathType;
+				if (data.propMediaSource != null) data.propMediaSource.enumValueIndex = (int)MediaSource.Path;
+
+				// Mark as modified
+				data.propPath.serializedObject.ApplyModifiedProperties();
+				foreach (Object o in data.propPath.serializedObject.targetObjects)
+				{
+					EditorUtility.SetDirty(o);
+				}
+
+				if (data.autoLoadMedia)
+				{
+					MediaPlayer mediaPlayer = (MediaPlayer)data.propPath.serializedObject.targetObject;
+					if (mediaPlayer != null)
+					{
+						mediaPlayer.OpenMedia(mediaPlayer.MediaPath, autoPlay:true);
+					}
+				}
+
+				RecentItems.Add(fullPath);
+			}
+		}
+
+		private void CreateMenu_RecentFiles(GenericMenu menu, List<string> items, string prefix, SerializedProperty propPath, SerializedProperty propMediaSource, bool autoLoadMedia)
+		{
+			int missingCount = 0;
+			for (int i = 0; i < items.Count; i++)
+			{
+				string path = items[i];
+				// Slashes in path must be replaced as they cause the menu to create submenuts
+				string itemName = ReplaceSlashes(path);
+				// TODO: shorten if itemName too long
+				if (System.IO.File.Exists(path))
+				{
+					menu.AddItem(new GUIContent(prefix + itemName), false, Callback_Select, new RecentMenuItemData(path, propPath, propMediaSource, autoLoadMedia));
+				}
+				else
+				{
+					menu.AddDisabledItem(new GUIContent(prefix + itemName));
+					missingCount++;
+				}
+			}
+			if (items.Count > 0)
+			{
+				menu.AddSeparator(prefix + "");
+				menu.AddItem(new GUIContent(prefix + "Clear"), false, Callback_ClearList, items);
+				if (missingCount > 0)
+				{
+					menu.AddItem(new GUIContent(prefix + "Clear Missing (" + missingCount + ")"), false, Callback_ClearMissingFiles);
+				}
+			}
+			else
+			{
+				menu.AddDisabledItem(new GUIContent(prefix + "No recent files yet"));
+			}
+		}
+
+		private void CreateMenu_RecentUrls(GenericMenu menu, List<string> items, string prefix, SerializedProperty propPath, SerializedProperty propMediaSource, bool autoLoadMedia)
+		{
+			for (int i = 0; i < items.Count; i++)
+			{
+				string path = items[i];
+				// Slashes in path must be replaced as they cause the menu to create submenuts
+				string itemName = ReplaceSlashes(path);
+				// TODO: shorten if itemName too long
+				menu.AddItem(new GUIContent(prefix + itemName), false, Callback_Select, new RecentMenuItemData(path, propPath, propMediaSource, autoLoadMedia));
+			}
+			if (items.Count > 0)
+			{
+				menu.AddSeparator(prefix + "");
+				menu.AddItem(new GUIContent(prefix + "Clear"), false, Callback_ClearList, items);
+			}
+			else
+			{
+				menu.AddDisabledItem(new GUIContent(prefix + "No recent URLs yet"));
+			}
+		}
+
+		private static string ReplaceSlashes(string text)
+		{
+			string slashReplacement = "\u2215";
+#if UNITY_EDITOR_WIN
+			// Special replacement for "//" in URLS so they aren't spaced too far apart
+			text = text.Replace("//", " \u2215 \u2215 ");
+
+			// On Windows we have to add extra spaces so it doesn't look squashed together
+			slashReplacement = " \u2215 ";
+#endif
+
+			text = text.Replace("/", slashReplacement).Replace("\\", slashReplacement);	
+
+			// Unity will place text after " _" on the right of the menu, so we replace it so this doesn't happen
+			text = text.Replace(" _", "_");
+
+			return text;
+		}
+
+		private static List<string> FindMediaFilesInStreamingAssetsFolder()
+		{
+			List<string> files = new List<string>();
+			if (System.IO.Directory.Exists(Application.streamingAssetsPath))
+			{
+				string[] allFiles = System.IO.Directory.GetFiles(Application.streamingAssetsPath, "*", System.IO.SearchOption.AllDirectories);
+				if (allFiles != null && allFiles.Length > 0)
+				{
+					// Filter by type
+					for (int i = 0; i < allFiles.Length; i++)
+					{
+						bool remove = false;
+						if (allFiles[i].EndsWith(".meta", System.StringComparison.InvariantCultureIgnoreCase))
+						{
+							remove = true;
+						}
+
+#if UNITY_EDITOR_OSX
+						remove = remove || allFiles[i].EndsWith(".DS_Store");
+#endif
+
+						if (!remove)
+						{
+							files.Add(allFiles[i]);
+						}
+					}
+				}
+			}
+			return files;
+		}
+
+		private void CreateMenu_StreamingAssets(GenericMenu menu, string prefix, SerializedProperty propPath, SerializedProperty propMediaSource, bool autoLoadMedia)
+		{
+			List<string> files = FindMediaFilesInStreamingAssetsFolder();
+			if (files.Count > 0)
+			{
+				for (int i = 0; i < files.Count; i++)
+				{
+					string path = files[i];
+					if (System.IO.File.Exists(path))
+					{
+						string itemName = path.Replace(Application.streamingAssetsPath, "");
+						if (itemName.StartsWith("/") || itemName.StartsWith("\\"))
+						{
+							itemName = itemName.Remove(0, 1);
+						}
+						itemName = itemName.Replace("\\", "/");
+
+						menu.AddItem(new GUIContent(prefix + itemName), false, Callback_Select, new RecentMenuItemData(path, propPath, propMediaSource, autoLoadMedia));
+					}
+				}
+			}
+			else
+			{
+				menu.AddDisabledItem(new GUIContent(prefix + "StreamingAssets folder missing or contains no files"));
+			}
+		}
+	}
+}

+ 12 - 0
package/Editor/Scripts/RecentMenu.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 58699a0e0590c6e4ba19c09d612e0bb2
+timeCreated: 1448902492
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 379 - 0
package/Editor/Scripts/SupportWindow.cs

@@ -0,0 +1,379 @@
+
+using UnityEngine;
+using UnityEditor;
+
+//-----------------------------------------------------------------------------
+// Copyright 2016-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+	/// <summary>
+	/// A window to display options to the user to help them report bugs
+	/// Also collects some metadata about the machine specs, plugin version etc
+	/// </summary>
+	public class SupportWindow : EditorWindow
+	{
+		private class MyPopupWindow : PopupWindowContent
+		{
+			private string _text;
+			private string _url;
+			private string _buttonMessage;
+
+			public MyPopupWindow(string text, string buttonMessage,string url)
+			{
+				_text = text;
+				_url = url;
+				_buttonMessage = buttonMessage;
+			}
+
+			public override Vector2 GetWindowSize()
+			{
+				return new Vector2(400, 520);
+			}
+
+			public override void OnGUI(Rect rect)
+			{
+				GUILayout.BeginHorizontal();
+				GUILayout.Label("Copy-Paste this text, then ", EditorStyles.boldLabel);
+				GUI.color = Color.green;
+				if (GUILayout.Button(_buttonMessage, GUILayout.ExpandWidth(true)))
+				{
+					Application.OpenURL(_url);
+				}
+				GUILayout.EndHorizontal();
+				GUI.color = Color.white;
+				EditorGUILayout.TextArea(_text);
+			}
+		}
+
+		private static bool _isCreated = false;
+		private static bool _isInit = false;
+
+		private const string SettingsPrefix = "AVProVideo.SupportWindow.";
+
+		private string _emailDescription = string.Empty;
+		private string _emailTopic = string.Empty;
+		private string _emailVideoFormat = string.Empty;
+		private string _emailDeviceSpecs = string.Empty;
+
+		//private bool _askForHelp = false;
+		private bool _trySelfSolve = false;
+		private Vector2 _scroll = Vector2.zero;
+
+		private int _selectionIndex = 0;
+		private static string[] _gridNames = { "Help Resources", "Ask for Help", "FAQ" };
+
+		[MenuItem("Window/AVPro Video Support")]
+		public static void Init()
+		{
+			// Close window if it is already open
+			if (_isInit || _isCreated)
+			{
+				SupportWindow window = (SupportWindow)EditorWindow.GetWindow(typeof(SupportWindow));
+				window.Close();
+				return;
+			}
+
+			_isCreated = true;
+
+			// Get existing open window or if none, make a new one:
+			SupportWindow window2 = ScriptableObject.CreateInstance<SupportWindow>();
+			if (window2 != null)
+			{
+				window2.SetupWindow();
+			}
+		}
+
+		private void SetupWindow()
+		{
+			_isCreated = true;
+			float width = 512f;
+			float height = 512f;
+			this.position = new Rect((Screen.width / 2) - (width / 2f), (Screen.height / 2) - (height / 2f), width, height);
+			this.minSize = new Vector2(530f, 510f);
+			this.titleContent = new GUIContent("AVPro Video - Help & Support");
+			this.CreateGUI();
+			LoadSettings();
+			this.ShowUtility();
+			this.Repaint();
+		}
+
+		private void CreateGUI()
+		{
+			_isInit = true;
+		}
+
+		void OnEnable()
+		{
+			if (!_isCreated)
+			{
+				SetupWindow();
+			}
+		}
+
+		void OnDisable()
+		{
+			_isInit = false;
+			_isCreated = false;
+			SaveSettings();
+			Repaint();
+		}
+
+		private void SaveSettings()
+		{
+			EditorPrefs.SetString(SettingsPrefix + "EmailTopic", _emailTopic);
+			EditorPrefs.SetString(SettingsPrefix + "EmailDescription", _emailDescription);
+			EditorPrefs.SetString(SettingsPrefix + "EmailDeviceSpecs", _emailDeviceSpecs);
+			EditorPrefs.SetString(SettingsPrefix + "EmailVideoSpecs", _emailVideoFormat);
+			EditorPrefs.SetBool(SettingsPrefix + "ExpandSelfSolve", _trySelfSolve);
+			EditorPrefs.SetInt(SettingsPrefix + "SelectionIndex", _selectionIndex);
+		}
+
+		private void LoadSettings()
+		{
+			_emailTopic = EditorPrefs.GetString(SettingsPrefix + "EmailTopic", _emailTopic);
+			_emailDescription = EditorPrefs.GetString(SettingsPrefix + "EmailDescription", _emailDescription);
+			_emailDeviceSpecs = EditorPrefs.GetString(SettingsPrefix + "EmailDeviceSpecs", _emailDeviceSpecs);
+			_emailVideoFormat = EditorPrefs.GetString(SettingsPrefix + "EmailVideoSpecs", _emailVideoFormat);
+			_trySelfSolve = EditorPrefs.GetBool(SettingsPrefix + "ExpandSelfSolve", _trySelfSolve);
+			_selectionIndex = EditorPrefs.GetInt(SettingsPrefix + "SelectionIndex", _selectionIndex);
+		}
+
+		private string CollectSupportData()
+		{
+			string nl = System.Environment.NewLine;
+
+			string version = string.Format("AVPro Video: v{0} (plugin v{1})", Helper.AVProVideoVersion, GetPluginVersion());
+			string targetPlatform = "Target Platform: " + EditorUserBuildSettings.selectedBuildTargetGroup.ToString();
+			string unityVersion = "Unity: v" + Application.unityVersion + " " + Application.platform.ToString();
+
+			string deviceInfo = "OS: " + SystemInfo.deviceType + " - " + SystemInfo.deviceModel + " - " + SystemInfo.operatingSystem + " - " + Application.systemLanguage;
+			string cpuInfo = "CPU: " + SystemInfo.processorType + " - " + SystemInfo.processorCount + " threads - " + + SystemInfo.systemMemorySize + "KB";
+			string gfxInfo = "GPU: " + SystemInfo.graphicsDeviceName + " - " + SystemInfo.graphicsDeviceVendor + " - " + SystemInfo.graphicsDeviceVersion + " - " + SystemInfo.graphicsMemorySize + "KB - " + SystemInfo.maxTextureSize;
+
+			return version + nl + targetPlatform + nl + unityVersion + nl + deviceInfo + nl + cpuInfo + nl + gfxInfo;
+		}
+
+		void OnGUI()
+		{
+			if (!_isInit)
+			{
+				EditorGUILayout.LabelField("Initialising...");
+				return;
+			}
+
+			GUILayout.Label("Having problems? We'll do our best to help.\n\nBelow is a collection of resources to help solve any issues you may encounter.", EditorStyles.wordWrappedLabel);
+			GUILayout.Space(16f);
+
+			/*GUI.color = Color.white;
+			GUI.backgroundColor = Color.clear;
+			if (_trySelfSolve)
+			{
+				GUI.color = Color.white;
+				GUI.backgroundColor = new Color(0.8f, 0.8f, 0.8f, 0.1f);
+				if (EditorGUIUtility.isProSkin)
+				{
+					GUI.backgroundColor = Color.black;
+				}
+			}
+			GUILayout.BeginVertical("box");
+			GUI.backgroundColor = Color.white;*/
+
+			_selectionIndex = GUILayout.Toolbar(_selectionIndex, _gridNames);
+
+			GUILayout.Space(16f);
+			/*if (GUILayout.Button("Try Solve the Issue Yourself", EditorStyles.toolbarButton))
+			{
+				//_trySelfSolve = !_trySelfSolve;
+				_trySelfSolve = true;
+			}
+			GUI.color = Color.white;
+			if (_trySelfSolve)*/
+			if (_selectionIndex == 0)
+			{
+				GUILayout.BeginHorizontal();
+				GUILayout.Label("1) ");
+				GUILayout.Label("Check you're using the latest version of AVPro Video via the Asset Store.  This is version " + Helper.AVProVideoVersion, EditorStyles.wordWrappedLabel);
+				GUILayout.FlexibleSpace();
+				GUILayout.EndHorizontal();
+
+				GUILayout.BeginHorizontal();
+				GUILayout.Label("2) ");
+				GUILayout.Label("Look at the example projects and scripts in the Demos folder");
+				GUILayout.FlexibleSpace();
+				GUILayout.EndHorizontal();
+
+				GUILayout.BeginHorizontal();
+				GUILayout.Label("3) ");
+				GUI.color = Color.green;
+				if (GUILayout.Button("Read the Documentation", GUILayout.ExpandWidth(false)))
+				{
+					Application.OpenURL(MediaPlayerEditor.LinkUserManual);
+				}
+				GUI.color = Color.white;
+				GUILayout.FlexibleSpace();
+				GUILayout.EndHorizontal();
+
+				GUILayout.BeginHorizontal();
+				GUILayout.Label("4) ");
+				GUI.color = Color.green;
+				if (GUILayout.Button("Read the GitHub Issues", GUILayout.ExpandWidth(false)))
+				{
+					Application.OpenURL(MediaPlayerEditor.LinkGithubIssues);
+				}
+				GUI.color = Color.white;
+				GUILayout.FlexibleSpace();
+				GUILayout.EndHorizontal();
+
+				GUILayout.BeginHorizontal();
+				GUILayout.Label("5) ");
+				GUI.color = Color.green;
+				if (GUILayout.Button("Read the Scripting Reference", GUILayout.ExpandWidth(false)))
+				{
+					Application.OpenURL(MediaPlayerEditor.LinkScriptingClassReference);
+				}
+				GUI.color = Color.white;
+				GUILayout.FlexibleSpace();
+				GUILayout.EndHorizontal();
+
+				GUILayout.BeginHorizontal();
+				GUILayout.Label("6) ");
+				GUI.color = Color.green;
+				if (GUILayout.Button("Visit the AVPro Video Website", GUILayout.ExpandWidth(false)))
+				{
+					Application.OpenURL(MediaPlayerEditor.LinkPluginWebsite);
+				}
+				GUI.color = Color.white;
+				GUILayout.FlexibleSpace();
+				GUILayout.EndHorizontal();
+
+				GUILayout.BeginHorizontal();
+				GUILayout.Label("7) ");
+				GUI.color = Color.green;
+				if (GUILayout.Button("Browse the Unity Forum", GUILayout.ExpandWidth(false)))
+				{
+					Application.OpenURL(MediaPlayerEditor.LinkForumPage);
+				}
+				GUI.color = Color.white;
+				GUILayout.FlexibleSpace();
+				GUILayout.EndHorizontal();
+			}
+			else if (_selectionIndex == 2)
+			{
+				GUILayout.Label("Coming soon...");
+			}
+			else if (_selectionIndex == 1)
+			{
+				GUILayout.Label("Please fill out these fields when sending us a new issue.\nThis makes it much easier and faster to resolve the issue.", EditorStyles.wordWrappedLabel);
+				GUILayout.Space(16f);
+
+				GUILayout.BeginVertical("box");
+				_scroll = GUILayout.BeginScrollView(_scroll);
+
+				GUILayout.Label("Issue/Question Title", EditorStyles.boldLabel);
+				_emailTopic = GUILayout.TextField(_emailTopic);
+
+				GUILayout.Space(8f);
+				GUILayout.Label("What's the problem?", EditorStyles.boldLabel);
+				_emailDescription = EditorGUILayout.TextArea(_emailDescription, GUILayout.Height(64f));
+
+				GUILayout.Space(8f);
+				GUILayout.BeginHorizontal();
+				GUILayout.Label("Tell us about your videos", EditorStyles.boldLabel);
+				GUILayout.Label("- Number of videos, resolution, codec, frame-rate, example URLs", EditorStyles.miniBoldLabel);
+				GUILayout.FlexibleSpace();
+				GUILayout.EndHorizontal();
+				_emailVideoFormat = EditorGUILayout.TextArea(_emailVideoFormat, GUILayout.Height(32f));
+
+				GUILayout.Space(8f);
+				GUILayout.BeginHorizontal();
+				GUILayout.Label("Which devices are you having the issue with?", EditorStyles.boldLabel);
+				GUILayout.Label("- Model, OS version number", EditorStyles.miniBoldLabel);
+				GUILayout.FlexibleSpace();
+				GUILayout.EndHorizontal();
+				_emailDeviceSpecs = EditorGUILayout.TextField(_emailDeviceSpecs);
+
+				//GUILayout.Space(16f);
+				////GUILayout.Label("System Information");
+				//GUILayout.TextArea(CollectSupportData());
+
+				string emailBody = System.Environment.NewLine + System.Environment.NewLine;
+				emailBody += "Problem description:" + System.Environment.NewLine + System.Environment.NewLine + _emailDescription + System.Environment.NewLine + System.Environment.NewLine;
+				emailBody += "Device (which devices are you having the issue with - model, OS version number):" + System.Environment.NewLine + System.Environment.NewLine + _emailDeviceSpecs + System.Environment.NewLine + System.Environment.NewLine;
+				emailBody += "Media (tell us about your videos - number of videos, resolution, codec, frame-rate, example URLs):" + System.Environment.NewLine + System.Environment.NewLine + _emailVideoFormat + System.Environment.NewLine + System.Environment.NewLine;
+				emailBody += "System Information:" + System.Environment.NewLine + System.Environment.NewLine + CollectSupportData() + System.Environment.NewLine + System.Environment.NewLine;
+
+				//GUILayout.Space(16f);
+//
+				//GUILayout.Label("Email Content");
+				//EditorGUILayout.TextArea(emailBody);
+
+				GUILayout.EndScrollView();
+				GUILayout.EndVertical();
+
+				GUILayout.Space(16f);
+
+				GUILayout.BeginHorizontal();
+				GUILayout.FlexibleSpace();
+				GUI.color = Color.green;
+				if (GUILayout.Button("Send at GitHub Issues ➔", GUILayout.ExpandWidth(false), GUILayout.Height(32f)))
+				{
+					PopupWindow.Show(buttonRect, new MyPopupWindow(emailBody, "Go to GitHub", MediaPlayerEditor.LinkGithubIssuesNew));
+				}
+				/*if (GUILayout.Button("Send at the Unity Forum ➔", GUILayout.ExpandWidth(false), GUILayout.Height(32f)))
+				{
+					PopupWindow.Show(buttonRect, new MyPopupWindow(emailBody, "Go to Forum", MediaPlayerEditor.LinkForumLastPage));
+				}*/
+
+				if (Event.current.type == EventType.Repaint)
+				{
+					buttonRect = GUILayoutUtility.GetLastRect();
+				}
+
+				GUI.color = Color.white;
+				GUILayout.FlexibleSpace();
+				GUILayout.EndHorizontal();
+			}
+			//GUILayout.EndVertical();
+
+			GUILayout.FlexibleSpace();
+
+			if (GUILayout.Button("Close"))
+			{
+				this.Close();
+			}			
+		}
+
+		private Rect buttonRect;
+
+		private struct Native
+		{
+#if UNITY_EDITOR_WIN
+			[System.Runtime.InteropServices.DllImport("AVProVideo")]
+			public static extern System.IntPtr GetPluginVersion();
+#elif UNITY_EDITOR_OSX
+			[System.Runtime.InteropServices.DllImport("AVProVideo")]
+			public static extern string AVPGetVersion();
+#endif
+		}
+
+		private static string GetPluginVersion()
+		{
+			string version = "Unknown";
+			try
+			{
+#if UNITY_EDITOR_WIN
+				version = System.Runtime.InteropServices.Marshal.PtrToStringAnsi(Native.GetPluginVersion());
+#elif UNITY_EDITOR_OSX
+				version = Native.AVPGetVersion();
+#endif
+			}
+			catch (System.DllNotFoundException e)
+			{
+				Debug.LogError("[AVProVideo] Failed to load DLL. " + e.Message);
+			}
+			return version;
+		}
+	}
+}

+ 8 - 0
package/Editor/Scripts/SupportWindow.cs.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: e7d92cb5b84798a44b49bb610befa0cf
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 

+ 71 - 0
package/Editor/Scripts/VideoResolveOptionsDrawer.cs

@@ -0,0 +1,71 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEditor;
+
+namespace RenderHeads.Media.AVProVideo.Editor
+{
+#if AVPRO_FEATURE_VIDEORESOLVE
+	[CustomPropertyDrawer(typeof(VideoResolveOptions))]
+	public class VideoResolveOptionsDrawer : PropertyDrawer
+	{
+		public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { return 0f; }
+
+		public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
+		{
+			EditorGUI.BeginProperty(position, GUIContent.none, property);
+
+			SerializedProperty propApplyHSBC = property.FindPropertyRelative("applyHSBC");
+			EditorGUILayout.PropertyField(propApplyHSBC, new GUIContent("Image Adjustments"));
+
+			if (propApplyHSBC.boolValue)
+			{
+				SerializedProperty propHue = property.FindPropertyRelative("hue");
+				SerializedProperty propSaturation = property.FindPropertyRelative("saturation");
+				SerializedProperty propBrightness = property.FindPropertyRelative("brightness");
+				SerializedProperty propContrast = property.FindPropertyRelative("contrast");
+				SerializedProperty propGamma = property.FindPropertyRelative("gamma");
+
+				EditorGUILayout.PropertyField(propHue);
+				EditorGUILayout.PropertyField(propSaturation);
+				EditorGUILayout.PropertyField(propBrightness);
+				EditorGUILayout.PropertyField(propContrast);
+				EditorGUILayout.PropertyField(propGamma);
+			}
+
+			{
+				SerializedProperty propTint = property.FindPropertyRelative("tint");
+				SerializedProperty propGenerateMipMaps = property.FindPropertyRelative("generateMipmaps");
+				EditorGUILayout.PropertyField(propTint);
+				EditorGUILayout.PropertyField(propGenerateMipMaps);
+			}
+
+			EditorGUI.EndProperty();
+		}
+	}
+
+	[CustomPropertyDrawer(typeof(VideoResolve))]
+	public class VideoResolveDrawer : PropertyDrawer
+	{
+		public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { return 0f; }
+
+		public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
+		{
+			EditorGUI.BeginProperty(position, GUIContent.none, property);
+
+			SerializedProperty propOptions = property.FindPropertyRelative("_options");
+			SerializedProperty propTargetRenderTexture = property.FindPropertyRelative("_targetRenderTexture");
+
+			EditorGUILayout.PropertyField(propOptions, true);
+			EditorGUILayout.PropertyField(propTargetRenderTexture, new GUIContent("Render Texture"));
+			if (propTargetRenderTexture.objectReferenceValue != null)
+			{
+				SerializedProperty propTargetRenderTextureScale = property.FindPropertyRelative("_targetRenderTextureScale");
+				EditorGUILayout.PropertyField(propTargetRenderTextureScale);
+			}
+
+			EditorGUI.EndProperty();
+		}
+	}	
+#endif
+}

+ 12 - 0
package/Editor/Scripts/VideoResolveOptionsDrawer.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 66da0bc3f380f71408dd648351fb36dd
+timeCreated: 1614875698
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 7 - 0
package/Editor/_AVProVideo.Editor.asmdef

@@ -0,0 +1,7 @@
+{
+    "name": "AVProVideo.Editor",
+    "references": [ "AVProVideo.Runtime" ],
+    "includePlatforms": [
+        "Editor"
+    ]
+}

+ 8 - 0
package/Editor/_AVProVideo.Editor.asmdef.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: b7e953b0f2cefdb479f1f95f12b72863
+timeCreated: 1546681457
+licenseType: Store
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
package/Platform.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: e4f6f97bde0c15c49a046b01e2bc74b7
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
package/Resources.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: c7507fd76e98a824caf1b3de8b5a843c
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
package/Resources/Resources.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: e1d4a7b544a19544a9ca52fcf18bfc78
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 0
package/Resources/Resources/Textures.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 4112ff91023ab1f44b54380a0431af99
+folderAsset: yes
+timeCreated: 1551713189
+licenseType: Store
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

BIN
package/Resources/Resources/Textures/AVProVideo-NullPlayer-Frame0.png


+ 90 - 0
package/Resources/Resources/Textures/AVProVideo-NullPlayer-Frame0.png.meta

@@ -0,0 +1,90 @@
+fileFormatVersion: 2
+guid: 8bef6179fcd26aa4c89b766c6b95490a
+TextureImporter:
+  fileIDToRecycleName: {}
+  serializedVersion: 4
+  mipmaps:
+    mipMapMode: 0
+    enableMipMap: 1
+    sRGBTexture: 1
+    linearTexture: 0
+    fadeOut: 0
+    borderMipMap: 0
+    mipMapFadeDistanceStart: 1
+    mipMapFadeDistanceEnd: 3
+  bumpmap:
+    convertToNormalMap: 0
+    externalNormalMap: 0
+    heightScale: 0.25
+    normalMapFilter: 0
+  isReadable: 0
+  grayScaleToAlpha: 0
+  generateCubemap: 6
+  cubemapConvolution: 0
+  seamlessCubemap: 0
+  textureFormat: -1
+  maxTextureSize: 2048
+  textureSettings:
+    filterMode: 2
+    aniso: -1
+    mipBias: -1
+    wrapMode: 1
+  nPOTScale: 0
+  lightmap: 0
+  compressionQuality: 50
+  spriteMode: 0
+  spriteExtrude: 1
+  spriteMeshType: 1
+  alignment: 0
+  spritePivot: {x: 0.5, y: 0.5}
+  spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+  spritePixelsToUnits: 100
+  alphaUsage: 0
+  alphaIsTransparency: 0
+  spriteTessellationDetail: -1
+  textureType: 0
+  textureShape: 1
+  maxTextureSizeSet: 0
+  compressionQualitySet: 0
+  textureFormatSet: 0
+  platformSettings:
+  - buildTarget: DefaultTexturePlatform
+    maxTextureSize: 2048
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+  - buildTarget: Standalone
+    maxTextureSize: 2048
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+  - buildTarget: iPhone
+    maxTextureSize: 2048
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+  - buildTarget: Android
+    maxTextureSize: 2048
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+  spriteSheet:
+    serializedVersion: 2
+    sprites: []
+    outline: []
+  spritePackingTag: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

BIN
package/Resources/Resources/Textures/AVProVideo-NullPlayer-Frame1.png


+ 90 - 0
package/Resources/Resources/Textures/AVProVideo-NullPlayer-Frame1.png.meta

@@ -0,0 +1,90 @@
+fileFormatVersion: 2
+guid: 855eae39e9698944daf581d77f6cef3c
+TextureImporter:
+  fileIDToRecycleName: {}
+  serializedVersion: 4
+  mipmaps:
+    mipMapMode: 0
+    enableMipMap: 1
+    sRGBTexture: 1
+    linearTexture: 0
+    fadeOut: 0
+    borderMipMap: 0
+    mipMapFadeDistanceStart: 1
+    mipMapFadeDistanceEnd: 3
+  bumpmap:
+    convertToNormalMap: 0
+    externalNormalMap: 0
+    heightScale: 0.25
+    normalMapFilter: 0
+  isReadable: 0
+  grayScaleToAlpha: 0
+  generateCubemap: 6
+  cubemapConvolution: 0
+  seamlessCubemap: 0
+  textureFormat: -1
+  maxTextureSize: 2048
+  textureSettings:
+    filterMode: 2
+    aniso: -1
+    mipBias: -1
+    wrapMode: 1
+  nPOTScale: 0
+  lightmap: 0
+  compressionQuality: 50
+  spriteMode: 0
+  spriteExtrude: 1
+  spriteMeshType: 1
+  alignment: 0
+  spritePivot: {x: 0.5, y: 0.5}
+  spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+  spritePixelsToUnits: 100
+  alphaUsage: 0
+  alphaIsTransparency: 0
+  spriteTessellationDetail: -1
+  textureType: 0
+  textureShape: 1
+  maxTextureSizeSet: 0
+  compressionQualitySet: 0
+  textureFormatSet: 0
+  platformSettings:
+  - buildTarget: DefaultTexturePlatform
+    maxTextureSize: 2048
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+  - buildTarget: Standalone
+    maxTextureSize: 2048
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+  - buildTarget: iPhone
+    maxTextureSize: 2048
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+  - buildTarget: Android
+    maxTextureSize: 2048
+    textureFormat: -1
+    textureCompression: 1
+    compressionQuality: 50
+    crunchedCompression: 0
+    allowsAlphaSplitting: 0
+    overridden: 0
+  spriteSheet:
+    serializedVersion: 2
+    sprites: []
+    outline: []
+  spritePackingTag: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
package/Resources/Scripts.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 4b4fede22d097844b8a5617eacb1385c
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 0
package/Resources/Scripts/AssetTypes.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 9c5f6cc7a68822c4c906fad89505801a
+folderAsset: yes
+timeCreated: 1592333515
+licenseType: Store
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 118 - 0
package/Resources/Scripts/AssetTypes/MediaReference.cs

@@ -0,0 +1,118 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace RenderHeads.Media.AVProVideo
+{
+	[System.Serializable]
+	[CreateAssetMenu(fileName = "MediaReference", menuName = "AVPro Video/Media Reference", order = 51)]
+	public class MediaReference : ScriptableObject
+	{
+		[SerializeField] string _alias = string.Empty;
+		public string Alias { get { return _alias; } set { _alias = value; } }
+
+		[SerializeField] MediaPath _mediaPath = new MediaPath();
+		public MediaPath MediaPath { get { return _mediaPath; } set { _mediaPath = value; } }
+
+		[Header("Media Hints")]
+
+		[SerializeField] MediaHints _hints = MediaHints.Default;
+		public MediaHints Hints { get { return _hints; } set { _hints = value; } }
+
+		[Header("Platform Overrides")]
+
+		[SerializeField] MediaReference _macOS = null;
+		[SerializeField] MediaReference _windows = null;
+		[SerializeField] MediaReference _android = null;
+		[SerializeField] MediaReference _iOS = null;
+		[SerializeField] MediaReference _tvOS = null;
+		[SerializeField] MediaReference _windowsUWP = null;
+		[SerializeField] MediaReference _webGL = null;
+
+#if UNITY_EDITOR
+		[SerializeField, HideInInspector] byte[] _preview = null;
+
+		public Texture2D GeneratePreview(Texture2D texture)
+		{
+			_preview = null;
+			if (texture)
+			{
+				texture.Apply(true, false);
+				_preview = texture.GetRawTextureData();
+			}
+			UnityEditor.EditorUtility.SetDirty(this);
+			return texture;
+		}
+
+		public bool GetPreview(Texture2D texture)
+		{
+			if (_preview != null && _preview.Length > 0 && _preview.Length > 128*128*4)
+			{
+				texture.LoadRawTextureData(_preview);
+				texture.Apply(true, false);
+				return true;
+			}
+			return false;
+		}
+#endif
+
+		public MediaReference GetCurrentPlatformMediaReference()
+		{
+			MediaReference result = null;
+
+		#if (UNITY_EDITOR_OSX && UNITY_IOS) || (!UNITY_EDITOR && UNITY_IOS)
+			result = GetPlatformMediaReference(Platform.iOS);
+		#elif (UNITY_EDITOR_OSX && UNITY_TVOS) || (!UNITY_EDITOR && UNITY_TVOS)
+			result = GetPlatformMediaReference(Platform.tvOS);
+		#elif (UNITY_EDITOR_OSX || (!UNITY_EDITOR && UNITY_STANDALONE_OSX))
+			result = GetPlatformMediaReference(Platform.MacOSX);
+		#elif (UNITY_EDITOR_WIN) || (!UNITY_EDITOR && UNITY_STANDALONE_WIN)
+			result = GetPlatformMediaReference(Platform.Windows);
+		#elif (!UNITY_EDITOR && UNITY_WSA_10_0)
+			result = GetPlatformMediaReference(Platform.WindowsUWP);
+		#elif (!UNITY_EDITOR && UNITY_ANDROID)
+			result = GetPlatformMediaReference(Platform.Android);
+		#elif (!UNITY_EDITOR && UNITY_WEBGL)
+			result = GetPlatformMediaReference(Platform.WebGL);
+		#endif
+
+			if (result == null)
+			{
+				result = this;
+			}
+
+			return result;
+		}
+
+		public MediaReference GetPlatformMediaReference(Platform platform)
+		{
+			MediaReference result = null;
+
+			switch (platform)
+			{
+				case Platform.iOS:
+					result = _iOS;
+					break;
+				case Platform.tvOS:
+					result = _tvOS;
+					break;
+				case Platform.MacOSX:
+					result = _macOS;
+					break;
+				case Platform.Windows:
+					result = _windows;
+					break;
+				case Platform.WindowsUWP:
+					result = _windowsUWP;
+					break;
+				case Platform.Android:
+					result = _android;
+					break;
+				case Platform.WebGL:
+					result = _webGL;
+					break;
+			}
+			return result;
+		}
+	}
+}

+ 12 - 0
package/Resources/Scripts/AssetTypes/MediaReference.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 8b1c70b7e7502564e93d418de9017d1f
+timeCreated: 1592337480
+licenseType: Store
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {fileID: 2800000, guid: bb83b41b53a59874692b83eab5873998, type: 3}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 0
package/Resources/Scripts/Components.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 40d7664ce355730488a96ff5305f1b5d
+folderAsset: yes
+timeCreated: 1438698284
+licenseType: Store
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 231 - 0
package/Resources/Scripts/Components/ApplyToMaterial.cs

@@ -0,0 +1,231 @@
+#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS || UNITY_TVOS
+	#define UNITY_PLATFORM_SUPPORTS_YPCBCR
+#endif
+
+using UnityEngine;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	/// <summary>
+	/// Sets up a material to display the video from a MediaPlayer
+	/// </summary>
+	[AddComponentMenu("AVPro Video/Apply To Material", 300)]
+	[HelpURL("https://www.renderheads.com/products/avpro-video/")]
+	public sealed class ApplyToMaterial : ApplyToBase
+	{
+		[Header("Display")]
+		[Space(8f)]
+
+		[Tooltip("Default texture to display when the video texture is preparing")]
+		[SerializeField] Texture2D _defaultTexture = null;
+
+		public Texture2D DefaultTexture
+		{
+			get { return _defaultTexture; }
+			set { if (_defaultTexture != value) { _defaultTexture = value; _isDirty = true; } }
+		}
+
+		[Space(8f)]
+		[Header("Material Target")]
+
+		[SerializeField] Material _material = null;
+
+		public Material Material
+		{
+			get { return _material; }
+			set { if (_material != value) { _material = value; _isDirty = true; } }
+		}
+
+		[SerializeField] string _texturePropertyName = Helper.UnityBaseTextureName;
+
+		public string TexturePropertyName
+		{
+			get { return _texturePropertyName; }
+			set
+			{
+				if (_texturePropertyName != value)
+				{
+					_texturePropertyName = value;
+					// TODO: if the property changes, remove it from the perioud SetTexture()
+					_propTexture = new LazyShaderProperty(_texturePropertyName);
+					_isDirty = true;
+				}
+			}
+		}
+
+		[SerializeField] Vector2 _offset = Vector2.zero;
+
+		public Vector2 Offset
+		{
+			get { return _offset; }
+			set { if (_offset != value) { _offset = value; _isDirty = true; } }
+		}
+
+		[SerializeField] Vector2 _scale = Vector2.one;
+
+		public Vector2 Scale
+		{
+			get { return _scale; }
+			set { if (_scale != value) { _scale = value; _isDirty = true; } }
+		}
+
+		private Texture _lastTextureApplied;
+		private LazyShaderProperty _propTexture;
+
+		private Texture _originalTexture;
+		private Vector2 _originalScale = Vector2.one;
+		private Vector2 _originalOffset = Vector2.zero;
+
+		// We do a LateUpdate() to allow for any changes in the texture that may have happened in Update()
+		private void LateUpdate()
+		{
+			Apply();
+		}
+
+		public override void Apply()
+		{
+			bool applied = false;
+
+			if (_media != null && _media.TextureProducer != null)
+			{
+				Texture resamplerTex = _media.FrameResampler == null || _media.FrameResampler.OutputTexture == null ? null : _media.FrameResampler.OutputTexture[0];
+				Texture texture = _media.UseResampler ? resamplerTex : _media.TextureProducer.GetTexture(0);
+				if (texture != null)
+				{
+					// Check for changing texture
+					if (texture != _lastTextureApplied)
+					{
+						_isDirty = true;
+					}
+
+					if (_isDirty)
+					{
+						int planeCount = _media.UseResampler ? 1 : _media.TextureProducer.GetTextureCount();
+						for (int plane = 0; plane < planeCount; ++plane)
+						{
+							Texture resamplerTexPlane = _media.FrameResampler == null || _media.FrameResampler.OutputTexture == null ? null : _media.FrameResampler.OutputTexture[plane];
+							texture = _media.UseResampler ? resamplerTexPlane : _media.TextureProducer.GetTexture(plane);
+							if (texture != null)
+							{
+								ApplyMapping(texture, _media.TextureProducer.RequiresVerticalFlip(), plane);
+							}
+						}
+					}
+					applied = true;
+				}
+			}
+
+			// If the media didn't apply a texture, then try to apply the default texture
+			if (!applied)
+			{
+				if (_defaultTexture != _lastTextureApplied)
+				{
+					_isDirty = true;
+				}
+				if (_isDirty)
+				{
+#if UNITY_PLATFORM_SUPPORTS_YPCBCR
+					if (_material != null && _material.HasProperty(VideoRender.PropUseYpCbCr.Id))
+					{
+						_material.DisableKeyword(VideoRender.Keyword_UseYpCbCr);
+					}
+#endif
+					ApplyMapping(_defaultTexture, false);
+				}
+			}
+		}
+
+
+		private void ApplyMapping(Texture texture, bool requiresYFlip, int plane = 0)
+		{
+			if (_material != null)
+			{
+				_isDirty = false;
+
+				if (plane == 0)
+				{
+					VideoRender.SetupMaterialForMedia(_material, _media, _propTexture.Id, texture, texture == _defaultTexture);
+					_lastTextureApplied = texture;
+
+					#if (!UNITY_EDITOR && UNITY_ANDROID)
+					if (texture == _defaultTexture)	{ _material.EnableKeyword("USING_DEFAULT_TEXTURE"); }
+					else							{ _material.DisableKeyword("USING_DEFAULT_TEXTURE"); }
+					#endif
+
+					if (texture != null)
+					{
+						if (requiresYFlip)
+						{
+							_material.SetTextureScale(_propTexture.Id, new Vector2(_scale.x, -_scale.y));
+							_material.SetTextureOffset(_propTexture.Id, Vector2.up + _offset);
+						}
+						else
+						{
+							_material.SetTextureScale(_propTexture.Id, _scale);
+							_material.SetTextureOffset(_propTexture.Id, _offset);
+						}
+					}
+				}
+				else if (plane == 1)
+				{
+					if (texture != null)
+					{
+						if (requiresYFlip)
+						{
+							_material.SetTextureScale(VideoRender.PropChromaTex.Id, new Vector2(_scale.x, -_scale.y));
+							_material.SetTextureOffset(VideoRender.PropChromaTex.Id, Vector2.up + _offset);
+						}
+						else
+						{
+							_material.SetTextureScale(VideoRender.PropChromaTex.Id, _scale);
+							_material.SetTextureOffset(VideoRender.PropChromaTex.Id, _offset);
+						}
+					}
+				}
+			}
+		}
+
+		protected override void SaveProperties()
+		{
+			if (_material != null)
+			{
+				if (string.IsNullOrEmpty(_texturePropertyName))
+				{
+					_originalTexture = _material.mainTexture;
+					_originalScale = _material.mainTextureScale;
+					_originalOffset = _material.mainTextureOffset;
+				}
+				else
+				{
+					_originalTexture = _material.GetTexture(_texturePropertyName);
+					_originalScale = _material.GetTextureScale(_texturePropertyName);
+					_originalOffset = _material.GetTextureOffset(_texturePropertyName);
+				}
+			}
+			_propTexture = new LazyShaderProperty(_texturePropertyName);
+		}
+
+		protected override void RestoreProperties()
+		{
+			if (_material != null)
+			{
+				if (string.IsNullOrEmpty(_texturePropertyName))
+				{
+					_material.mainTexture = _originalTexture;
+					_material.mainTextureScale = _originalScale;
+					_material.mainTextureOffset = _originalOffset;
+				}
+				else
+				{
+					_material.SetTexture(_texturePropertyName, _originalTexture);
+					_material.SetTextureScale(_texturePropertyName, _originalScale);
+					_material.SetTextureOffset(_texturePropertyName, _originalOffset);
+				}
+			}
+		}
+	}
+}

+ 8 - 0
package/Resources/Scripts/Components/ApplyToMaterial.cs.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d2feedce2e2e63647b8f875ec0894a15
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {fileID: 2800000, guid: bb83b41b53a59874692b83eab5873998, type: 3}
+  userData: 

+ 248 - 0
package/Resources/Scripts/Components/ApplyToMesh.cs

@@ -0,0 +1,248 @@
+using UnityEngine;
+using UnityEngine.Serialization;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	/// <summary>
+	/// Sets up a mesh to display the video from a MediaPlayer
+	/// </summary>
+	[AddComponentMenu("AVPro Video/Apply To Mesh", 300)]
+	[HelpURL("https://www.renderheads.com/products/avpro-video/")]
+	public sealed class ApplyToMesh : ApplyToBase
+	{
+		// TODO: add specific material / material index to target in the mesh if there are multiple materials
+
+		[Space(8f)]
+		[Header("Display")]
+
+		[Tooltip("Default texture to display when the video texture is preparing")]
+		[SerializeField] Texture2D _defaultTexture = null;
+
+		public Texture2D DefaultTexture
+		{
+			get { return _defaultTexture; }
+			set { ChangeDefaultTexture(value); }
+		}
+
+		[Space(8f)]
+		[FormerlySerializedAs("_mesh")]
+		[Header("Renderer Target")]
+		[SerializeField] Renderer _renderer = null;
+
+		public Renderer MeshRenderer
+		{
+			get { return _renderer; }
+			set { ChangeRenderer(value); }
+		}
+
+		[SerializeField] int _materialIndex = -1;
+
+		public int MaterialIndex
+		{
+			get { return _materialIndex; }
+			set { _materialIndex = value; }
+		}
+
+		private void ChangeDefaultTexture(Texture2D texture)
+		{
+			if (_defaultTexture != texture)
+			{
+				_defaultTexture = texture;
+				ForceUpdate();
+			}
+		}
+		private void ChangeRenderer(Renderer renderer)
+		{
+			if (_renderer != renderer)
+			{
+				if (_renderer)
+				{
+					// TODO: Remove from renderer
+				}
+				_renderer = renderer;
+				if (_renderer)
+				{
+					ForceUpdate();
+				}
+			}
+		}
+
+		[SerializeField] string _texturePropertyName = Helper.UnityBaseTextureName;
+
+		public string TexturePropertyName
+		{
+			get { return _texturePropertyName; }
+			set
+			{
+				if (_texturePropertyName != value)
+				{
+					_texturePropertyName = value;
+					// TODO: if the property changes, remove it from the perioud SetTexture()
+					_propTexture = new LazyShaderProperty(_texturePropertyName);
+					_isDirty = true;
+				}
+			}
+		}
+
+		[SerializeField] Vector2 _offset = Vector2.zero;
+
+		public Vector2 Offset
+		{
+			get { return _offset; }
+			set { if (_offset != value) { _offset = value; _isDirty = true; } }
+		}
+
+		[SerializeField] Vector2 _scale = Vector2.one;
+
+		public Vector2 Scale
+		{
+			get { return _scale; }
+			set { if (_scale != value) { _scale = value; _isDirty = true; } }
+		}
+
+		private Texture _lastTextureApplied;
+		private LazyShaderProperty _propTexture;
+
+		// We do a LateUpdate() to allow for any changes in the texture that may have happened in Update()
+		private void LateUpdate()
+		{
+			Apply();
+		}
+
+		public override void Apply()
+		{
+			bool applied = false;
+
+			// Try to apply texture from media
+			if (_media != null && _media.TextureProducer != null)
+			{
+				Texture resamplerTex = _media.FrameResampler == null || _media.FrameResampler.OutputTexture == null ? null : _media.FrameResampler.OutputTexture[0];
+				Texture texture = _media.UseResampler ? resamplerTex : _media.TextureProducer.GetTexture(0);
+				if (texture != null)
+				{
+					// Check for changing texture
+					if (texture != _lastTextureApplied)
+					{
+						_isDirty = true;
+					}
+
+					if (_isDirty)
+					{
+						int planeCount = _media.UseResampler ? 1 : _media.TextureProducer.GetTextureCount();
+						for (int plane = 0; plane < planeCount; plane++)
+						{
+							Texture resamplerTexPlane = _media.FrameResampler == null || _media.FrameResampler.OutputTexture == null ? null : _media.FrameResampler.OutputTexture[plane];
+							texture = _media.UseResampler ? resamplerTexPlane : _media.TextureProducer.GetTexture(plane);
+							if (texture != null)
+							{
+								ApplyMapping(texture, _media.TextureProducer.RequiresVerticalFlip(), plane, _materialIndex);
+							}
+						}
+					}
+					applied = true;
+				}
+			}
+
+			// If the media didn't apply a texture, then try to apply the default texture
+			if (!applied)
+			{
+				if (_defaultTexture != _lastTextureApplied)
+				{
+					_isDirty = true;
+				}
+				if (_isDirty)
+				{
+					ApplyMapping(_defaultTexture, false, 0, _materialIndex);
+				}
+			}
+		}
+
+		private void ApplyMapping(Texture texture, bool requiresYFlip, int plane, int materialIndex = -1)
+		{
+			if (_renderer != null)
+			{
+				_isDirty = false;
+
+				Material[] meshMaterials = _renderer.materials;
+				if (meshMaterials != null)
+				{
+					for (int i = 0; i < meshMaterials.Length; i++)
+					{
+						if (_materialIndex < 0 || i == _materialIndex)
+						{
+							Material mat = meshMaterials[i];
+							if (mat != null)
+							{
+								if (plane == 0)
+								{
+									VideoRender.SetupMaterialForMedia(mat, _media, _propTexture.Id, texture, texture == _defaultTexture);
+									_lastTextureApplied = texture;
+
+									#if (!UNITY_EDITOR && UNITY_ANDROID)
+									if(texture == _defaultTexture)	{ mat.EnableKeyword("USING_DEFAULT_TEXTURE"); }
+									else							{ mat.DisableKeyword("USING_DEFAULT_TEXTURE"); }
+									#endif
+
+									if (texture != null)
+									{
+										if (requiresYFlip)
+										{
+											mat.SetTextureScale(_propTexture.Id, new Vector2(_scale.x, -_scale.y));
+											mat.SetTextureOffset(_propTexture.Id, Vector2.up + _offset);
+										}
+										else
+										{
+											mat.SetTextureScale(_propTexture.Id, _scale);
+											mat.SetTextureOffset(_propTexture.Id, _offset);
+										}
+									}
+								}
+								else if (plane == 1)
+								{
+									if (texture != null)
+									{
+										if (requiresYFlip)
+										{
+											mat.SetTextureScale(VideoRender.PropChromaTex.Id, new Vector2(_scale.x, -_scale.y));
+											mat.SetTextureOffset(VideoRender.PropChromaTex.Id, Vector2.up + _offset);
+										}
+										else
+										{
+											mat.SetTextureScale(VideoRender.PropChromaTex.Id, _scale);
+											mat.SetTextureOffset(VideoRender.PropChromaTex.Id, _offset);
+										}
+									}
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+
+		protected override void OnEnable()
+		{
+			if (_renderer == null)
+			{
+				_renderer = this.GetComponent<MeshRenderer>();
+				if (_renderer == null)
+				{
+					Debug.LogWarning("[AVProVideo] No MeshRenderer set or found in gameobject");
+				}
+			}
+
+			_propTexture = new LazyShaderProperty(_texturePropertyName);
+
+			ForceUpdate();
+		}
+
+		protected override void OnDisable()
+		{
+			ApplyMapping(_defaultTexture, false, 0, _materialIndex);
+		}
+	}
+}

+ 8 - 0
package/Resources/Scripts/Components/ApplyToMesh.cs.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: f6d1977a52888584496b1acc7e998011
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {fileID: 2800000, guid: bb83b41b53a59874692b83eab5873998, type: 3}
+  userData: 

+ 81 - 0
package/Resources/Scripts/Components/AudioChannelMixer.cs

@@ -0,0 +1,81 @@
+using UnityEngine;
+
+//-----------------------------------------------------------------------------
+// Copyright 2019-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	/// Allows per-channel volume control
+	/// Currently supported on Windows and UWP (Media Foundation API only), macOS, iOS, tvOS and Android (ExoPlayer API only)
+	[AddComponentMenu("AVPro Video/Audio Channel Mixer", 401)]
+	[HelpURL("https://www.renderheads.com/products/avpro-video/")]
+	public class AudioChannelMixer : MonoBehaviour
+	{
+		const int MaxChannels = 8;
+
+		[Range(0f, 1f)]
+		[SerializeField] float[] _channels = null;
+
+		/// Range 0.0 to 1.0
+		public float[] Channel
+		{
+			get { return _channels; }
+			set { _channels = value; }
+		}
+
+		void Reset()
+		{
+			_channels = new float[MaxChannels];
+			for (int i = 0; i < MaxChannels; i++)
+			{
+				_channels[i] = 1f;
+			}
+		}
+
+		void ChangeChannelCount(int numChannels)
+		{
+			float[] channels = new float[numChannels];
+			if (_channels != null && _channels.Length != 0)
+			{
+				for (int i = 0; i < channels.Length; i++)
+				{
+					if (i < _channels.Length)
+					{
+						channels[i] = _channels[i];
+					}
+					else
+					{
+						channels[i] = 1f;
+					}
+				}
+			}
+			else
+			{
+				for (int i = 0; i < numChannels; i++)
+				{
+					channels[i] = 1f;
+				}
+			}
+			_channels = channels;
+		}
+
+		void OnAudioFilterRead(float[] data, int channels)
+		{
+			if (channels != _channels.Length)
+			{
+				ChangeChannelCount(channels);
+			}
+			int k = 0;
+			int numSamples = data.Length / channels;
+			for (int j = 0; j < numSamples; j++)
+			{
+				for (int i = 0; i < channels; i++)
+				{
+					data[k] *= _channels[i];
+					k++;
+				}
+			}
+		}
+	}
+}

Some files were not shown because too many files changed in this diff