Browse Source

添加AVProVideo 插件 修改用户上传的自定义图片和视频,在手机端无法移动、旋转及缩放

DGJ 1 year ago
parent
commit
31393fd9e6
100 changed files with 14501 additions and 0 deletions
  1. 8 0
      Assets/AVProVideo.meta
  2. 9 0
      Assets/AVProVideo/Runtime.meta
  3. 9 0
      Assets/AVProVideo/Runtime/Plugins.meta
  4. 9 0
      Assets/AVProVideo/Runtime/Plugins/WSA.meta
  5. 9 0
      Assets/AVProVideo/Runtime/Plugins/WSA/UWP.meta
  6. 5 0
      Assets/AVProVideo/Runtime/Plugins/WSA/UWP/ARM.meta
  7. 23 0
      Assets/AVProVideo/Runtime/Plugins/WSA/UWP/ARM/AVProVideo.dll.meta
  8. 149 0
      Assets/AVProVideo/Runtime/Plugins/WSA/UWP/ARM/AVProVideoWinRT.dll.meta
  9. 5 0
      Assets/AVProVideo/Runtime/Plugins/WSA/UWP/ARM64.meta
  10. 94 0
      Assets/AVProVideo/Runtime/Plugins/WSA/UWP/ARM64/AVProVideo.dll.meta
  11. 149 0
      Assets/AVProVideo/Runtime/Plugins/WSA/UWP/ARM64/AVProVideoWinRT.dll.meta
  12. 5 0
      Assets/AVProVideo/Runtime/Plugins/WSA/UWP/x86.meta
  13. 23 0
      Assets/AVProVideo/Runtime/Plugins/WSA/UWP/x86/AVProVideo.dll.meta
  14. 149 0
      Assets/AVProVideo/Runtime/Plugins/WSA/UWP/x86/AVProVideoWinRT.dll.meta
  15. 34 0
      Assets/AVProVideo/Runtime/Plugins/WSA/UWP/x86/Audio360.dll.meta
  16. 5 0
      Assets/AVProVideo/Runtime/Plugins/WSA/UWP/x86_64.meta
  17. 57 0
      Assets/AVProVideo/Runtime/Plugins/WSA/UWP/x86_64/AVProVideo.dll.meta
  18. 150 0
      Assets/AVProVideo/Runtime/Plugins/WSA/UWP/x86_64/AVProVideoWinRT.dll.meta
  19. 57 0
      Assets/AVProVideo/Runtime/Plugins/WSA/UWP/x86_64/Audio360.dll.meta
  20. 9 0
      Assets/AVProVideo/Runtime/Plugins/Windows.meta
  21. 9 0
      Assets/AVProVideo/Runtime/Plugins/Windows/x86_64.meta
  22. 135 0
      Assets/AVProVideo/Runtime/Plugins/Windows/x86_64/AVProVideo.dll.meta
  23. 135 0
      Assets/AVProVideo/Runtime/Plugins/Windows/x86_64/AVProVideoWinRT.dll.meta
  24. 9 0
      Assets/AVProVideo/Runtime/Resources.meta
  25. 9 0
      Assets/AVProVideo/Runtime/Resources/Textures.meta
  26. BIN
      Assets/AVProVideo/Runtime/Resources/Textures/AVProVideo-NullPlayer-Frame0.png
  27. 90 0
      Assets/AVProVideo/Runtime/Resources/Textures/AVProVideo-NullPlayer-Frame0.png.meta
  28. BIN
      Assets/AVProVideo/Runtime/Resources/Textures/AVProVideo-NullPlayer-Frame1.png
  29. 90 0
      Assets/AVProVideo/Runtime/Resources/Textures/AVProVideo-NullPlayer-Frame1.png.meta
  30. 9 0
      Assets/AVProVideo/Runtime/Scripts.meta
  31. 9 0
      Assets/AVProVideo/Runtime/Scripts/AssetTypes.meta
  32. 118 0
      Assets/AVProVideo/Runtime/Scripts/AssetTypes/MediaReference.cs
  33. 12 0
      Assets/AVProVideo/Runtime/Scripts/AssetTypes/MediaReference.cs.meta
  34. 9 0
      Assets/AVProVideo/Runtime/Scripts/Components.meta
  35. 231 0
      Assets/AVProVideo/Runtime/Scripts/Components/ApplyToMaterial.cs
  36. 8 0
      Assets/AVProVideo/Runtime/Scripts/Components/ApplyToMaterial.cs.meta
  37. 253 0
      Assets/AVProVideo/Runtime/Scripts/Components/ApplyToMesh.cs
  38. 8 0
      Assets/AVProVideo/Runtime/Scripts/Components/ApplyToMesh.cs.meta
  39. 81 0
      Assets/AVProVideo/Runtime/Scripts/Components/AudioChannelMixer.cs
  40. 8 0
      Assets/AVProVideo/Runtime/Scripts/Components/AudioChannelMixer.cs.meta
  41. 179 0
      Assets/AVProVideo/Runtime/Scripts/Components/AudioOutput.cs
  42. 8 0
      Assets/AVProVideo/Runtime/Scripts/Components/AudioOutput.cs.meta
  43. 321 0
      Assets/AVProVideo/Runtime/Scripts/Components/DisplayIMGUI.cs
  44. 12 0
      Assets/AVProVideo/Runtime/Scripts/Components/DisplayIMGUI.cs.meta
  45. 1477 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer.cs
  46. 12 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer.cs.meta
  47. 469 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayerSync.cs
  48. 12 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayerSync.cs.meta
  49. 65 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_AppFocus.cs
  50. 12 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_AppFocus.cs.meta
  51. 29 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_EditorMute.cs
  52. 12 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_EditorMute.cs.meta
  53. 83 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_EditorPlayPause.cs
  54. 12 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_EditorPlayPause.cs.meta
  55. 269 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_Events.cs
  56. 12 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_Events.cs.meta
  57. 211 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_ExtractFrame.cs
  58. 12 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_ExtractFrame.cs.meta
  59. 129 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_OpenBuffer.cs
  60. 12 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_OpenBuffer.cs.meta
  61. 60 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_OpenStream.cs
  62. 12 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_OpenStream.cs.meta
  63. 716 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_PlatformOptions.cs
  64. 12 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_PlatformOptions.cs.meta
  65. 150 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_Subtitles.cs
  66. 12 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_Subtitles.cs.meta
  67. 93 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_TimeScale.cs
  68. 12 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_TimeScale.cs.meta
  69. 83 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_Upgrade.cs
  70. 12 0
      Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_Upgrade.cs.meta
  71. 1012 0
      Assets/AVProVideo/Runtime/Scripts/Components/PlaylistMediaPlayer.cs
  72. 17 0
      Assets/AVProVideo/Runtime/Scripts/Components/PlaylistMediaPlayer.cs.meta
  73. 156 0
      Assets/AVProVideo/Runtime/Scripts/Components/ResolveToRenderTexture.cs
  74. 12 0
      Assets/AVProVideo/Runtime/Scripts/Components/ResolveToRenderTexture.cs.meta
  75. 178 0
      Assets/AVProVideo/Runtime/Scripts/Components/UpdateMultiPassStereo.cs
  76. 12 0
      Assets/AVProVideo/Runtime/Scripts/Components/UpdateMultiPassStereo.cs.meta
  77. 5 0
      Assets/AVProVideo/Runtime/Scripts/Internal.meta
  78. 124 0
      Assets/AVProVideo/Runtime/Scripts/Internal/ApplyToBase.cs
  79. 11 0
      Assets/AVProVideo/Runtime/Scripts/Internal/ApplyToBase.cs.meta
  80. 215 0
      Assets/AVProVideo/Runtime/Scripts/Internal/AudioOutputManager.cs
  81. 12 0
      Assets/AVProVideo/Runtime/Scripts/Internal/AudioOutputManager.cs.meta
  82. 652 0
      Assets/AVProVideo/Runtime/Scripts/Internal/BaseMediaPlayer.cs
  83. 12 0
      Assets/AVProVideo/Runtime/Scripts/Internal/BaseMediaPlayer.cs.meta
  84. 106 0
      Assets/AVProVideo/Runtime/Scripts/Internal/Events.cs
  85. 12 0
      Assets/AVProVideo/Runtime/Scripts/Internal/Events.cs.meta
  86. 519 0
      Assets/AVProVideo/Runtime/Scripts/Internal/Helper.cs
  87. 12 0
      Assets/AVProVideo/Runtime/Scripts/Internal/Helper.cs.meta
  88. 982 0
      Assets/AVProVideo/Runtime/Scripts/Internal/Interfaces.cs
  89. 12 0
      Assets/AVProVideo/Runtime/Scripts/Internal/Interfaces.cs.meta
  90. 225 0
      Assets/AVProVideo/Runtime/Scripts/Internal/PlaybackQualityStats.cs
  91. 12 0
      Assets/AVProVideo/Runtime/Scripts/Internal/PlaybackQualityStats.cs.meta
  92. 7 0
      Assets/AVProVideo/Runtime/Scripts/Internal/Players.meta
  93. 1725 0
      Assets/AVProVideo/Runtime/Scripts/Internal/Players/AndroidMediaPlayer.cs
  94. 12 0
      Assets/AVProVideo/Runtime/Scripts/Internal/Players/AndroidMediaPlayer.cs.meta
  95. 439 0
      Assets/AVProVideo/Runtime/Scripts/Internal/Players/AppleMediaPlayer+Native.cs
  96. 11 0
      Assets/AVProVideo/Runtime/Scripts/Internal/Players/AppleMediaPlayer+Native.cs.meta
  97. 1069 0
      Assets/AVProVideo/Runtime/Scripts/Internal/Players/AppleMediaPlayer.cs
  98. 12 0
      Assets/AVProVideo/Runtime/Scripts/Internal/Players/AppleMediaPlayer.cs.meta
  99. 226 0
      Assets/AVProVideo/Runtime/Scripts/Internal/Players/AppleMediaPlayerExtensions.cs
  100. 11 0
      Assets/AVProVideo/Runtime/Scripts/Internal/Players/AppleMediaPlayerExtensions.cs.meta

+ 8 - 0
Assets/AVProVideo.meta

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

+ 9 - 0
Assets/AVProVideo/Runtime.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 89ee47ef22faf9540bb381f817a762e6
+folderAsset: yes
+timeCreated: 1551709592
+licenseType: Pro
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 0
Assets/AVProVideo/Runtime/Plugins.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 44a4f634cbe11d749a36d11bf779d560
+folderAsset: yes
+timeCreated: 1541807141
+licenseType: Pro
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 0
Assets/AVProVideo/Runtime/Plugins/WSA.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: fa62f77468bf8a8489ef0a72c3e51ce0
+folderAsset: yes
+timeCreated: 1466635509
+licenseType: Pro
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 0
Assets/AVProVideo/Runtime/Plugins/WSA/UWP.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 0015a9d8f1e31bd4c8af7c8195dab886
+folderAsset: yes
+timeCreated: 1466636212
+licenseType: Pro
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 5 - 0
Assets/AVProVideo/Runtime/Plugins/WSA/UWP/ARM.meta

@@ -0,0 +1,5 @@
+fileFormatVersion: 2
+guid: da5cd71ba09f0a548ac774e50236a6f7
+folderAsset: yes
+DefaultImporter:
+  userData: 

+ 23 - 0
Assets/AVProVideo/Runtime/Plugins/WSA/UWP/ARM/AVProVideo.dll.meta

@@ -0,0 +1,23 @@
+fileFormatVersion: 2
+guid: 855efe2872a032d4fb533d8bbf373a96
+PluginImporter:
+  serializedVersion: 1
+  iconMap: {}
+  executionOrder: {}
+  isPreloaded: 0
+  platformData:
+    Any:
+      enabled: 0
+      settings: {}
+    Editor:
+      enabled: 0
+      settings:
+        DefaultValueInitialized: true
+    WindowsStoreApps:
+      enabled: 1
+      settings:
+        CPU: ARM
+        SDK: UWP
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 149 - 0
Assets/AVProVideo/Runtime/Plugins/WSA/UWP/ARM/AVProVideoWinRT.dll.meta

@@ -0,0 +1,149 @@
+fileFormatVersion: 2
+guid: 1d02b97c55097e94aa26100231b66a52
+timeCreated: 1543352727
+licenseType: Pro
+PluginImporter:
+  serializedVersion: 2
+  iconMap: {}
+  executionOrder: {}
+  isPreloaded: 0
+  isOverridable: 0
+  platformData:
+    data:
+      first:
+        '': Any
+      second:
+        enabled: 0
+        settings:
+          Exclude Android: 1
+          Exclude Editor: 1
+          Exclude Linux: 1
+          Exclude Linux64: 1
+          Exclude LinuxUniversal: 1
+          Exclude OSXIntel: 1
+          Exclude OSXIntel64: 1
+          Exclude OSXUniversal: 1
+          Exclude Win: 1
+          Exclude Win64: 1
+          Exclude WindowsStoreApps: 0
+          Exclude iOS: 1
+    data:
+      first:
+        '': Editor
+      second:
+        enabled: 0
+        settings:
+          CPU: AnyCPU
+          OS: AnyOS
+    data:
+      first:
+        Android: Android
+      second:
+        enabled: 0
+        settings:
+          CPU: ARMv7
+    data:
+      first:
+        Any: 
+      second:
+        enabled: 0
+        settings: {}
+    data:
+      first:
+        Editor: Editor
+      second:
+        enabled: 0
+        settings:
+          DefaultValueInitialized: true
+    data:
+      first:
+        Facebook: Win
+      second:
+        enabled: 0
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Facebook: Win64
+      second:
+        enabled: 0
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Standalone: Linux
+      second:
+        enabled: 0
+        settings:
+          CPU: x86
+    data:
+      first:
+        Standalone: Linux64
+      second:
+        enabled: 0
+        settings:
+          CPU: x86_64
+    data:
+      first:
+        Standalone: LinuxUniversal
+      second:
+        enabled: 0
+        settings:
+          CPU: None
+    data:
+      first:
+        Standalone: OSXIntel
+      second:
+        enabled: 0
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Standalone: OSXIntel64
+      second:
+        enabled: 0
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Standalone: OSXUniversal
+      second:
+        enabled: 0
+        settings:
+          CPU: None
+    data:
+      first:
+        Standalone: Win
+      second:
+        enabled: 0
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Standalone: Win64
+      second:
+        enabled: 0
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Windows Store Apps: WindowsStoreApps
+      second:
+        enabled: 1
+        settings:
+          CPU: ARM
+          DontProcess: False
+          PlaceholderPath: 
+          SDK: UWP
+          ScriptingBackend: AnyScriptingBackend
+    data:
+      first:
+        iPhone: iOS
+      second:
+        enabled: 0
+        settings:
+          CompileFlags: 
+          FrameworkDependencies: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 5 - 0
Assets/AVProVideo/Runtime/Plugins/WSA/UWP/ARM64.meta

@@ -0,0 +1,5 @@
+fileFormatVersion: 2
+guid: f55aa2578ac5b0449a3a3e50387f569f
+folderAsset: yes
+DefaultImporter:
+  userData: 

+ 94 - 0
Assets/AVProVideo/Runtime/Plugins/WSA/UWP/ARM64/AVProVideo.dll.meta

@@ -0,0 +1,94 @@
+fileFormatVersion: 2
+guid: 60594004122890048a42c162b1715315
+PluginImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  iconMap: {}
+  executionOrder: {}
+  defineConstraints: []
+  isPreloaded: 0
+  isOverridable: 0
+  isExplicitlyReferenced: 0
+  validateReferences: 1
+  platformData:
+  - first:
+      '': Any
+    second:
+      enabled: 0
+      settings:
+        Exclude Editor: 1
+        Exclude Linux: 1
+        Exclude Linux64: 1
+        Exclude LinuxUniversal: 1
+        Exclude OSXUniversal: 1
+        Exclude Win: 1
+        Exclude Win64: 1
+        Exclude WindowsStoreApps: 0
+  - first:
+      Any: 
+    second:
+      enabled: 0
+      settings: {}
+  - first:
+      Editor: Editor
+    second:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+        DefaultValueInitialized: true
+        OS: AnyOS
+  - first:
+      Facebook: Win
+    second:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+  - first:
+      Facebook: Win64
+    second:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+  - first:
+      Standalone: Linux
+    second:
+      enabled: 0
+      settings:
+        CPU: x86
+  - first:
+      Standalone: Linux64
+    second:
+      enabled: 0
+      settings:
+        CPU: x86_64
+  - first:
+      Standalone: OSXUniversal
+    second:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+  - first:
+      Standalone: Win
+    second:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+  - first:
+      Standalone: Win64
+    second:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+  - first:
+      Windows Store Apps: WindowsStoreApps
+    second:
+      enabled: 1
+      settings:
+        CPU: ARM64
+        DontProcess: false
+        PlaceholderPath: 
+        SDK: UWP
+        ScriptingBackend: AnyScriptingBackend
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 149 - 0
Assets/AVProVideo/Runtime/Plugins/WSA/UWP/ARM64/AVProVideoWinRT.dll.meta

@@ -0,0 +1,149 @@
+fileFormatVersion: 2
+guid: 1d02b97c55098e94aa26100231b66a52
+timeCreated: 1543352727
+licenseType: Pro
+PluginImporter:
+  serializedVersion: 2
+  iconMap: {}
+  executionOrder: {}
+  isPreloaded: 0
+  isOverridable: 0
+  platformData:
+    data:
+      first:
+        '': Any
+      second:
+        enabled: 0
+        settings:
+          Exclude Android: 1
+          Exclude Editor: 1
+          Exclude Linux: 1
+          Exclude Linux64: 1
+          Exclude LinuxUniversal: 1
+          Exclude OSXIntel: 1
+          Exclude OSXIntel64: 1
+          Exclude OSXUniversal: 1
+          Exclude Win: 1
+          Exclude Win64: 1
+          Exclude WindowsStoreApps: 0
+          Exclude iOS: 1
+    data:
+      first:
+        '': Editor
+      second:
+        enabled: 0
+        settings:
+          CPU: AnyCPU
+          OS: AnyOS
+    data:
+      first:
+        Android: Android
+      second:
+        enabled: 0
+        settings:
+          CPU: ARMv7
+    data:
+      first:
+        Any: 
+      second:
+        enabled: 0
+        settings: {}
+    data:
+      first:
+        Editor: Editor
+      second:
+        enabled: 0
+        settings:
+          DefaultValueInitialized: true
+    data:
+      first:
+        Facebook: Win
+      second:
+        enabled: 0
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Facebook: Win64
+      second:
+        enabled: 0
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Standalone: Linux
+      second:
+        enabled: 0
+        settings:
+          CPU: x86
+    data:
+      first:
+        Standalone: Linux64
+      second:
+        enabled: 0
+        settings:
+          CPU: x86_64
+    data:
+      first:
+        Standalone: LinuxUniversal
+      second:
+        enabled: 0
+        settings:
+          CPU: None
+    data:
+      first:
+        Standalone: OSXIntel
+      second:
+        enabled: 0
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Standalone: OSXIntel64
+      second:
+        enabled: 0
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Standalone: OSXUniversal
+      second:
+        enabled: 0
+        settings:
+          CPU: None
+    data:
+      first:
+        Standalone: Win
+      second:
+        enabled: 0
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Standalone: Win64
+      second:
+        enabled: 0
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Windows Store Apps: WindowsStoreApps
+      second:
+        enabled: 1
+        settings:
+          CPU: ARM64
+          DontProcess: False
+          PlaceholderPath: 
+          SDK: UWP
+          ScriptingBackend: AnyScriptingBackend
+    data:
+      first:
+        iPhone: iOS
+      second:
+        enabled: 0
+        settings:
+          CompileFlags: 
+          FrameworkDependencies: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 5 - 0
Assets/AVProVideo/Runtime/Plugins/WSA/UWP/x86.meta

@@ -0,0 +1,5 @@
+fileFormatVersion: 2
+guid: 8eccc1911a8d14b4b8e46658ff18b2c4
+folderAsset: yes
+DefaultImporter:
+  userData: 

+ 23 - 0
Assets/AVProVideo/Runtime/Plugins/WSA/UWP/x86/AVProVideo.dll.meta

@@ -0,0 +1,23 @@
+fileFormatVersion: 2
+guid: 647235627694b5843b3b3461bda59fd8
+PluginImporter:
+  serializedVersion: 1
+  iconMap: {}
+  executionOrder: {}
+  isPreloaded: 0
+  platformData:
+    Any:
+      enabled: 0
+      settings: {}
+    Editor:
+      enabled: 0
+      settings:
+        DefaultValueInitialized: true
+    WindowsStoreApps:
+      enabled: 1
+      settings:
+        CPU: x86
+        SDK: UWP
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 149 - 0
Assets/AVProVideo/Runtime/Plugins/WSA/UWP/x86/AVProVideoWinRT.dll.meta

@@ -0,0 +1,149 @@
+fileFormatVersion: 2
+guid: 9dba3755b2ce1ea4c8f455e959bc1bfa
+timeCreated: 1543330553
+licenseType: Pro
+PluginImporter:
+  serializedVersion: 2
+  iconMap: {}
+  executionOrder: {}
+  isPreloaded: 0
+  isOverridable: 0
+  platformData:
+    data:
+      first:
+        '': Any
+      second:
+        enabled: 0
+        settings:
+          Exclude Android: 1
+          Exclude Editor: 1
+          Exclude Linux: 1
+          Exclude Linux64: 1
+          Exclude LinuxUniversal: 1
+          Exclude OSXIntel: 1
+          Exclude OSXIntel64: 1
+          Exclude OSXUniversal: 1
+          Exclude Win: 1
+          Exclude Win64: 1
+          Exclude WindowsStoreApps: 0
+          Exclude iOS: 1
+    data:
+      first:
+        '': Editor
+      second:
+        enabled: 0
+        settings:
+          CPU: AnyCPU
+          OS: AnyOS
+    data:
+      first:
+        Android: Android
+      second:
+        enabled: 0
+        settings:
+          CPU: ARMv7
+    data:
+      first:
+        Any: 
+      second:
+        enabled: 0
+        settings: {}
+    data:
+      first:
+        Editor: Editor
+      second:
+        enabled: 0
+        settings:
+          DefaultValueInitialized: true
+    data:
+      first:
+        Facebook: Win
+      second:
+        enabled: 0
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Facebook: Win64
+      second:
+        enabled: 0
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Standalone: Linux
+      second:
+        enabled: 0
+        settings:
+          CPU: x86
+    data:
+      first:
+        Standalone: Linux64
+      second:
+        enabled: 0
+        settings:
+          CPU: x86_64
+    data:
+      first:
+        Standalone: LinuxUniversal
+      second:
+        enabled: 0
+        settings:
+          CPU: None
+    data:
+      first:
+        Standalone: OSXIntel
+      second:
+        enabled: 0
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Standalone: OSXIntel64
+      second:
+        enabled: 0
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Standalone: OSXUniversal
+      second:
+        enabled: 0
+        settings:
+          CPU: None
+    data:
+      first:
+        Standalone: Win
+      second:
+        enabled: 0
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Standalone: Win64
+      second:
+        enabled: 0
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Windows Store Apps: WindowsStoreApps
+      second:
+        enabled: 1
+        settings:
+          CPU: X86
+          DontProcess: False
+          PlaceholderPath: 
+          SDK: UWP
+          ScriptingBackend: AnyScriptingBackend
+    data:
+      first:
+        iPhone: iOS
+      second:
+        enabled: 0
+        settings:
+          CompileFlags: 
+          FrameworkDependencies: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 34 - 0
Assets/AVProVideo/Runtime/Plugins/WSA/UWP/x86/Audio360.dll.meta

@@ -0,0 +1,34 @@
+fileFormatVersion: 2
+guid: 947235627694b5843b3b3461bda59fd9
+PluginImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  iconMap: {}
+  executionOrder: {}
+  defineConstraints: []
+  isPreloaded: 0
+  isOverridable: 0
+  isExplicitlyReferenced: 0
+  validateReferences: 1
+  platformData:
+  - first:
+      Any: 
+    second:
+      enabled: 0
+      settings: {}
+  - first:
+      Editor: Editor
+    second:
+      enabled: 0
+      settings:
+        DefaultValueInitialized: true
+  - first:
+      Windows Store Apps: WindowsStoreApps
+    second:
+      enabled: 1
+      settings:
+        CPU: X86
+        SDK: UWP
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 5 - 0
Assets/AVProVideo/Runtime/Plugins/WSA/UWP/x86_64.meta

@@ -0,0 +1,5 @@
+fileFormatVersion: 2
+guid: f55aa2578ac5b0449a3a6e50387f569f
+folderAsset: yes
+DefaultImporter:
+  userData: 

+ 57 - 0
Assets/AVProVideo/Runtime/Plugins/WSA/UWP/x86_64/AVProVideo.dll.meta

@@ -0,0 +1,57 @@
+fileFormatVersion: 2
+guid: 3b25c194f2b9d8d48a0cd13422c0fbe7
+PluginImporter:
+  serializedVersion: 1
+  iconMap: {}
+  executionOrder: {}
+  isPreloaded: 0
+  platformData:
+    Any:
+      enabled: 0
+      settings: {}
+    Editor:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+        DefaultValueInitialized: true
+        OS: AnyOS
+    Linux:
+      enabled: 0
+      settings:
+        CPU: x86
+    Linux64:
+      enabled: 0
+      settings:
+        CPU: x86_64
+    OSXIntel:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+    OSXIntel64:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+    Win:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+    Win64:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+    WindowsStoreApps:
+      enabled: 1
+      settings:
+        CPU: X64
+        DontProcess: False
+        PlaceholderPath: 
+        SDK: UWP
+        ScriptingBackend: AnyScriptingBackend
+    iOS:
+      enabled: 0
+      settings:
+        CompileFlags: 
+        FrameworkDependencies: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 150 - 0
Assets/AVProVideo/Runtime/Plugins/WSA/UWP/x86_64/AVProVideoWinRT.dll.meta

@@ -0,0 +1,150 @@
+fileFormatVersion: 2
+guid: f0dcaa0e9423f364cadec44ece659971
+timeCreated: 1543330778
+licenseType: Pro
+PluginImporter:
+  serializedVersion: 2
+  iconMap: {}
+  executionOrder: {}
+  isPreloaded: 0
+  isOverridable: 0
+  platformData:
+    data:
+      first:
+        '': Any
+      second:
+        enabled: 0
+        settings:
+          Exclude Android: 1
+          Exclude Editor: 1
+          Exclude Linux: 1
+          Exclude Linux64: 1
+          Exclude LinuxUniversal: 1
+          Exclude OSXIntel: 1
+          Exclude OSXIntel64: 1
+          Exclude OSXUniversal: 1
+          Exclude Win: 1
+          Exclude Win64: 1
+          Exclude WindowsStoreApps: 0
+          Exclude iOS: 1
+    data:
+      first:
+        '': Editor
+      second:
+        enabled: 0
+        settings:
+          CPU: x86_64
+          OS: Windows
+    data:
+      first:
+        Android: Android
+      second:
+        enabled: 0
+        settings:
+          CPU: ARMv7
+    data:
+      first:
+        Any: 
+      second:
+        enabled: 0
+        settings: {}
+    data:
+      first:
+        Editor: Editor
+      second:
+        enabled: 0
+        settings:
+          CPU: x86_64
+          DefaultValueInitialized: true
+    data:
+      first:
+        Facebook: Win
+      second:
+        enabled: 0
+        settings:
+          CPU: None
+    data:
+      first:
+        Facebook: Win64
+      second:
+        enabled: 1
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Standalone: Linux
+      second:
+        enabled: 0
+        settings:
+          CPU: None
+    data:
+      first:
+        Standalone: Linux64
+      second:
+        enabled: 0
+        settings:
+          CPU: x86_64
+    data:
+      first:
+        Standalone: LinuxUniversal
+      second:
+        enabled: 0
+        settings:
+          CPU: None
+    data:
+      first:
+        Standalone: OSXIntel
+      second:
+        enabled: 0
+        settings:
+          CPU: None
+    data:
+      first:
+        Standalone: OSXIntel64
+      second:
+        enabled: 0
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Standalone: OSXUniversal
+      second:
+        enabled: 0
+        settings:
+          CPU: None
+    data:
+      first:
+        Standalone: Win
+      second:
+        enabled: 0
+        settings:
+          CPU: None
+    data:
+      first:
+        Standalone: Win64
+      second:
+        enabled: 0
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Windows Store Apps: WindowsStoreApps
+      second:
+        enabled: 1
+        settings:
+          CPU: X64
+          DontProcess: False
+          PlaceholderPath: 
+          SDK: UWP
+          ScriptingBackend: AnyScriptingBackend
+    data:
+      first:
+        iPhone: iOS
+      second:
+        enabled: 0
+        settings:
+          CompileFlags: 
+          FrameworkDependencies: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 57 - 0
Assets/AVProVideo/Runtime/Plugins/WSA/UWP/x86_64/Audio360.dll.meta

@@ -0,0 +1,57 @@
+fileFormatVersion: 2
+guid: 9b25c194f2b9d8d48a0cd13422c0fbe9
+PluginImporter:
+  serializedVersion: 1
+  iconMap: {}
+  executionOrder: {}
+  isPreloaded: 0
+  platformData:
+    Any:
+      enabled: 0
+      settings: {}
+    Editor:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+        DefaultValueInitialized: true
+        OS: AnyOS
+    Linux:
+      enabled: 0
+      settings:
+        CPU: x86
+    Linux64:
+      enabled: 0
+      settings:
+        CPU: x86_64
+    OSXIntel:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+    OSXIntel64:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+    Win:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+    Win64:
+      enabled: 0
+      settings:
+        CPU: AnyCPU
+    WindowsStoreApps:
+      enabled: 1
+      settings:
+        CPU: X64
+        DontProcess: False
+        PlaceholderPath: 
+        SDK: UWP
+        ScriptingBackend: AnyScriptingBackend
+    iOS:
+      enabled: 0
+      settings:
+        CompileFlags: 
+        FrameworkDependencies: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 0
Assets/AVProVideo/Runtime/Plugins/Windows.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: b6d2cf61b759e1f458f8c7fd862ee0ef
+folderAsset: yes
+timeCreated: 1591798085
+licenseType: Pro
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 0
Assets/AVProVideo/Runtime/Plugins/Windows/x86_64.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 29897f3c1b37b574e87b1de35e3706a0
+folderAsset: yes
+timeCreated: 1611680629
+licenseType: Pro
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 135 - 0
Assets/AVProVideo/Runtime/Plugins/Windows/x86_64/AVProVideo.dll.meta

@@ -0,0 +1,135 @@
+fileFormatVersion: 2
+guid: 47103a0dd0066fb4b8e31c75c49c2f04
+PluginImporter:
+  serializedVersion: 2
+  iconMap: {}
+  executionOrder: {}
+  isPreloaded: 0
+  isOverridable: 0
+  platformData:
+    data:
+      first:
+        '': Any
+      second:
+        enabled: 0
+        settings:
+          Exclude Editor: 0
+          Exclude Linux: 1
+          Exclude Linux64: 0
+          Exclude LinuxUniversal: 1
+          Exclude OSXIntel: 1
+          Exclude OSXIntel64: 0
+          Exclude OSXUniversal: 1
+          Exclude Win: 1
+          Exclude Win64: 0
+    data:
+      first:
+        '': Editor
+      second:
+        enabled: 0
+        settings:
+          CPU: x86_64
+          OS: Windows
+    data:
+      first:
+        Android: Android
+      second:
+        enabled: 0
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Any: 
+      second:
+        enabled: 0
+        settings: {}
+    data:
+      first:
+        Editor: Editor
+      second:
+        enabled: 1
+        settings:
+          CPU: x86_64
+          DefaultValueInitialized: true
+          OS: Windows
+    data:
+      first:
+        Facebook: Win
+      second:
+        enabled: 0
+        settings:
+          CPU: None
+    data:
+      first:
+        Facebook: Win64
+      second:
+        enabled: 1
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Standalone: Linux
+      second:
+        enabled: 0
+        settings:
+          CPU: None
+    data:
+      first:
+        Standalone: Linux64
+      second:
+        enabled: 1
+        settings:
+          CPU: x86_64
+    data:
+      first:
+        Standalone: LinuxUniversal
+      second:
+        enabled: 0
+        settings:
+          CPU: x86_64
+    data:
+      first:
+        Standalone: OSXIntel
+      second:
+        enabled: 0
+        settings:
+          CPU: None
+    data:
+      first:
+        Standalone: OSXIntel64
+      second:
+        enabled: 1
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Standalone: OSXUniversal
+      second:
+        enabled: 0
+        settings:
+          CPU: x86_64
+    data:
+      first:
+        Standalone: Win
+      second:
+        enabled: 0
+        settings:
+          CPU: None
+    data:
+      first:
+        Standalone: Win64
+      second:
+        enabled: 1
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        iPhone: iOS
+      second:
+        enabled: 0
+        settings:
+          CompileFlags: 
+          FrameworkDependencies: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 135 - 0
Assets/AVProVideo/Runtime/Plugins/Windows/x86_64/AVProVideoWinRT.dll.meta

@@ -0,0 +1,135 @@
+fileFormatVersion: 2
+guid: 47103a0dd0066fb4b8e31c75c49c2ee4
+PluginImporter:
+  serializedVersion: 2
+  iconMap: {}
+  executionOrder: {}
+  isPreloaded: 0
+  isOverridable: 0
+  platformData:
+    data:
+      first:
+        '': Any
+      second:
+        enabled: 0
+        settings:
+          Exclude Editor: 0
+          Exclude Linux: 1
+          Exclude Linux64: 0
+          Exclude LinuxUniversal: 1
+          Exclude OSXIntel: 1
+          Exclude OSXIntel64: 0
+          Exclude OSXUniversal: 1
+          Exclude Win: 1
+          Exclude Win64: 0
+    data:
+      first:
+        '': Editor
+      second:
+        enabled: 0
+        settings:
+          CPU: x86_64
+          OS: Windows
+    data:
+      first:
+        Android: Android
+      second:
+        enabled: 0
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Any: 
+      second:
+        enabled: 0
+        settings: {}
+    data:
+      first:
+        Editor: Editor
+      second:
+        enabled: 1
+        settings:
+          CPU: x86_64
+          DefaultValueInitialized: true
+          OS: Windows
+    data:
+      first:
+        Facebook: Win
+      second:
+        enabled: 0
+        settings:
+          CPU: None
+    data:
+      first:
+        Facebook: Win64
+      second:
+        enabled: 1
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Standalone: Linux
+      second:
+        enabled: 0
+        settings:
+          CPU: None
+    data:
+      first:
+        Standalone: Linux64
+      second:
+        enabled: 1
+        settings:
+          CPU: x86_64
+    data:
+      first:
+        Standalone: LinuxUniversal
+      second:
+        enabled: 0
+        settings:
+          CPU: x86_64
+    data:
+      first:
+        Standalone: OSXIntel
+      second:
+        enabled: 0
+        settings:
+          CPU: None
+    data:
+      first:
+        Standalone: OSXIntel64
+      second:
+        enabled: 1
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        Standalone: OSXUniversal
+      second:
+        enabled: 0
+        settings:
+          CPU: x86_64
+    data:
+      first:
+        Standalone: Win
+      second:
+        enabled: 0
+        settings:
+          CPU: None
+    data:
+      first:
+        Standalone: Win64
+      second:
+        enabled: 1
+        settings:
+          CPU: AnyCPU
+    data:
+      first:
+        iPhone: iOS
+      second:
+        enabled: 0
+        settings:
+          CompileFlags: 
+          FrameworkDependencies: 
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 0
Assets/AVProVideo/Runtime/Resources.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 870241155d4fedc4a86fa89eb85ae5c3
+folderAsset: yes
+timeCreated: 1449047212
+licenseType: Free
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 0
Assets/AVProVideo/Runtime/Resources/Textures.meta

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

BIN
Assets/AVProVideo/Runtime/Resources/Textures/AVProVideo-NullPlayer-Frame0.png


+ 90 - 0
Assets/AVProVideo/Runtime/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
Assets/AVProVideo/Runtime/Resources/Textures/AVProVideo-NullPlayer-Frame1.png


+ 90 - 0
Assets/AVProVideo/Runtime/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: 

+ 9 - 0
Assets/AVProVideo/Runtime/Scripts.meta

@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: 87c76c122fc365242b155cea199f02b4
+folderAsset: yes
+timeCreated: 1551713189
+licenseType: Pro
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 9 - 0
Assets/AVProVideo/Runtime/Scripts/AssetTypes.meta

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

+ 118 - 0
Assets/AVProVideo/Runtime/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
Assets/AVProVideo/Runtime/Scripts/AssetTypes/MediaReference.cs.meta

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

+ 9 - 0
Assets/AVProVideo/Runtime/Scripts/Components.meta

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

+ 231 - 0
Assets/AVProVideo/Runtime/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
Assets/AVProVideo/Runtime/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: 

+ 253 - 0
Assets/AVProVideo/Runtime/Scripts/Components/ApplyToMesh.cs

@@ -0,0 +1,253 @@
+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 void SetUrl(string url)
+		{
+			this._media.MediaPath.PathType = MediaPathType.AbsolutePathOrURL;
+            this._media.MediaPath.Path = url;
+		}
+		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
Assets/AVProVideo/Runtime/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
Assets/AVProVideo/Runtime/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++;
+				}
+			}
+		}
+	}
+}

+ 8 - 0
Assets/AVProVideo/Runtime/Scripts/Components/AudioChannelMixer.cs.meta

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

+ 179 - 0
Assets/AVProVideo/Runtime/Scripts/Components/AudioOutput.cs

@@ -0,0 +1,179 @@
+using UnityEngine;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	/// <summary>
+	/// Audio is grabbed from the MediaPlayer and rendered via Unity AudioSource
+	/// This allows audio to have 3D spatial control, effects applied and to be spatialised for VR
+	/// Currently supported on Windows and UWP (Media Foundation API only), macOS, iOS, tvOS and Android (ExoPlayer API only)
+	/// </summary>
+	[RequireComponent(typeof(AudioSource))]
+	[AddComponentMenu("AVPro Video/Audio Output", 400)]
+	[HelpURL("https://www.renderheads.com/products/avpro-video/")]
+	public class AudioOutput : MonoBehaviour
+	{
+		public enum AudioOutputMode
+		{
+			OneToAllChannels,
+			MultipleChannels
+		}
+
+		[SerializeField] MediaPlayer _mediaPlayer = null;
+		[SerializeField] AudioOutputMode _audioOutputMode = AudioOutputMode.MultipleChannels;
+		[HideInInspector, SerializeField] int _channelMask = 0xffff;
+		[SerializeField] bool _supportPositionalAudio = false;
+
+		public MediaPlayer Player
+		{
+			get { return _mediaPlayer; }
+			set { ChangeMediaPlayer(value); }
+		}
+
+		public AudioOutputMode OutputMode
+		{
+			get { return _audioOutputMode; }
+			set { _audioOutputMode = value; }
+		}
+
+		public int ChannelMask
+		{
+			get { return _channelMask; }
+			set { _channelMask = value; }
+		}
+
+		private AudioSource _audioSource;
+
+		void Awake()
+		{
+			_audioSource = this.GetComponent<AudioSource>();
+			Debug.Assert(_audioSource != null);
+		}
+
+		void Start()
+		{
+			AudioSettings.OnAudioConfigurationChanged += OnAudioConfigurationChanged;
+			ChangeMediaPlayer(_mediaPlayer);
+		}
+
+		void OnAudioConfigurationChanged(bool deviceChanged)
+		{
+			if (_mediaPlayer.Control == null)
+				return;
+			_mediaPlayer.Control.AudioConfigurationChanged(deviceChanged);
+		}
+
+		void OnDestroy()
+		{
+			ChangeMediaPlayer(null);
+		}
+
+		void Update()
+		{
+			if (_mediaPlayer != null && _mediaPlayer.Control != null && _mediaPlayer.Control.IsPlaying())
+			{
+				ApplyAudioSettings(_mediaPlayer, _audioSource);
+			}
+		}
+
+		public AudioSource GetAudioSource()
+		{
+			return _audioSource;
+		}
+
+		public void ChangeMediaPlayer(MediaPlayer newPlayer)
+		{
+			#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || (!UNITY_EDITOR && (UNITY_IOS || UNITY_TVOS))
+				if (newPlayer != null)
+				{
+					MediaPlayer.OptionsApple options = (MediaPlayer.OptionsApple)newPlayer.GetCurrentPlatformOptions();
+					if (options.audioMode == MediaPlayer.OptionsApple.AudioMode.Unity)
+					{
+						this.enabled = true;
+					}
+					else
+					{
+						Debug.LogWarning("[AVProVideo] Unity audio output is not supported when 'Audio Output Mode' is not set to 'Unity' in the MediaPlayer platform options");
+						this.enabled = false;
+						return;
+					}
+				}
+			#endif
+
+			// When changing the media player, handle event subscriptions
+			if (_mediaPlayer != null)
+			{
+				_mediaPlayer.AudioSource = null;
+				_mediaPlayer.Events.RemoveListener(OnMediaPlayerEvent);
+				_mediaPlayer = null;
+			}
+
+			_mediaPlayer = newPlayer;
+			if (_mediaPlayer != null)
+			{
+				_mediaPlayer.Events.AddListener(OnMediaPlayerEvent);
+				_mediaPlayer.AudioSource = _audioSource;
+			}
+
+			if (_supportPositionalAudio)
+			{
+				if (_audioSource.clip == null)
+				{
+					// Position audio is implemented from hints found on this thread:
+					// https://forum.unity.com/threads/onaudiofilterread-sound-spatialisation.362782/
+					int frameCount = 2048 * 10;
+					int sampleCount = frameCount * Helper.GetUnityAudioSpeakerCount();
+					AudioClip clip = AudioClip.Create("dummy", frameCount, Helper.GetUnityAudioSpeakerCount(), Helper.GetUnityAudioSampleRate(), false);
+					float[] samples = new float[sampleCount];
+					for (int i = 0; i < samples.Length; i++) { samples[i] = 1f; }
+					clip.SetData(samples, 0);
+					_audioSource.clip = clip;
+					_audioSource.loop = true;
+				}
+			}
+			else if (_audioSource.clip != null)
+			{
+				_audioSource.clip = null;
+			}
+		}
+
+		// Callback function to handle events
+		private void OnMediaPlayerEvent(MediaPlayer mp, MediaPlayerEvent.EventType et, ErrorCode errorCode)
+		{
+			switch (et)
+			{
+				case MediaPlayerEvent.EventType.Closing:
+					_audioSource.Stop();
+					break;
+				case MediaPlayerEvent.EventType.Started:
+					ApplyAudioSettings(_mediaPlayer, _audioSource);
+					_audioSource.Play();
+					break;
+			}
+		}
+
+		private static void ApplyAudioSettings(MediaPlayer player, AudioSource audioSource)
+		{
+			// Apply volume and mute from the MediaPlayer to the AudioSource
+			if (audioSource != null && player != null && player.Control != null)
+			{
+				float volume = player.Control.GetVolume();
+				bool isMuted = player.Control.IsMuted();
+				float rate = player.Control.GetPlaybackRate();
+				audioSource.volume = volume;
+				audioSource.mute = isMuted;
+				audioSource.pitch = rate;
+			}
+		}
+
+#if (UNITY_EDITOR_WIN || UNITY_EDITOR_OSX) || (!UNITY_EDITOR && (UNITY_STANDALONE_WIN || UNITY_WSA_10_0 || UNITY_STANDALONE_OSX || UNITY_IOS || UNITY_TVOS || UNITY_ANDROID))
+		void OnAudioFilterRead(float[] audioData, int channelCount)
+		{
+			AudioOutputManager.Instance.RequestAudio(this, _mediaPlayer, audioData, channelCount, _channelMask, _audioOutputMode, _supportPositionalAudio);
+		}
+#endif
+	}
+}

+ 8 - 0
Assets/AVProVideo/Runtime/Scripts/Components/AudioOutput.cs.meta

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

+ 321 - 0
Assets/AVProVideo/Runtime/Scripts/Components/DisplayIMGUI.cs

@@ -0,0 +1,321 @@
+#if UNITY_EDITOR || UNITY_STANDALONE_OSX || UNITY_STANDALONE_WIN || UNITY_IOS || UNITY_TVOS || UNITY_ANDROID || (UNITY_WEBGL && UNITY_2017_2_OR_NEWER)
+	#define UNITY_PLATFORM_SUPPORTS_LINEAR
+#endif
+#if (UNITY_EDITOR_WIN || (!UNITY_EDITOR && UNITY_STANDALONE_WIN))
+	#define UNITY_PLATFORM_SUPPORTS_VIDEOASPECTRATIO
+#endif
+
+using UnityEngine;
+using UnityEngine.Serialization;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	/// <summary>
+	/// Displays the video from MediaPlayer component using IMGUI
+	/// </summary>
+	[AddComponentMenu("AVPro Video/Display IMGUI", 200)]
+	[HelpURL("https://www.renderheads.com/products/avpro-video/")]
+	[ExecuteInEditMode]
+	public class DisplayIMGUI : MonoBehaviour
+	{
+		[SerializeField] MediaPlayer _mediaPlayer = null;
+		public MediaPlayer Player
+		{
+			get { return _mediaPlayer; }
+			set { _mediaPlayer = value; Update(); }
+		}
+
+		[SerializeField] ScaleMode	_scaleMode	= ScaleMode.ScaleToFit;
+		public ScaleMode ScaleMode { get { return _scaleMode; } set { _scaleMode = value; } }
+
+		[SerializeField] Color _color = UnityEngine.Color.white;
+		public Color Color { get { return _color; } set { _color = value; } }
+
+		[FormerlySerializedAs("_alphaBlend")]
+		[SerializeField] bool _allowTransparency = false;
+		public bool AllowTransparency { get { return _allowTransparency; } set { _allowTransparency = value; } }
+
+		[SerializeField] bool _useDepth = false;
+		public bool UseDepth { get { return _useDepth; } set { _useDepth = value; } }
+
+		[SerializeField] int _depth = 0;
+		public int Depth { get { return _depth; } set { _depth = value; } }
+
+		[Header("Area")]
+
+		[FormerlySerializedAs("_fullScreen")]
+		[SerializeField] bool _isAreaFullScreen = true;
+		public bool IsAreaFullScreen { get { return _isAreaFullScreen; } set { _isAreaFullScreen = value; } }
+
+		[FormerlySerializedAs("_x")]
+		[Range(0f, 1f)]
+		[SerializeField] float _areaX = 0f;
+		public float AreaX { get { return _areaX; } set { _areaX = value; } }
+
+		[FormerlySerializedAs("_y")]
+		[Range(0f, 1f)]
+		[SerializeField] float _areaY = 0f;
+		public float AreaY { get { return _areaY; } set { _areaY = value; } }
+
+		[FormerlySerializedAs("_width")]
+		[Range(0f, 1f)]
+		[SerializeField] float _areaWidth = 1f;
+		public float AreaWidth { get { return _areaWidth; } set { _areaWidth = value; } }
+
+		[FormerlySerializedAs("_height")]
+		[Range(0f, 1f)]
+		[SerializeField] float _areaHeight = 1f;
+		public float AreaHeight { get { return _areaHeight; } set { _areaHeight = value; } }
+
+		[FormerlySerializedAs("_displayInEditor")]
+		[SerializeField] bool _showAreaInEditor = false;
+		public bool ShowAreaInEditor { get { return _showAreaInEditor; } set { _showAreaInEditor = value; } }
+
+		private static Shader	_shaderAlphaPacking;
+		private Material		_material;
+
+		void Start()
+		{
+			// Disabling useGUILayout lets you skip the GUI layout phase which helps performance, but this also breaks the GUI.depth usage.
+			if (!_useDepth)
+			{
+				this.useGUILayout = false;
+			}
+
+			if (!_shaderAlphaPacking)
+			{
+				_shaderAlphaPacking = Shader.Find("AVProVideo/Internal/IMGUI/Texture Transparent");
+				if (!_shaderAlphaPacking)
+				{
+					Debug.LogWarning("[AVProVideo] Missing shader 'AVProVideo/Internal/IMGUI/Texture Transparent'");
+				}
+			}
+		}
+
+		public void Update()
+		{
+			if (_mediaPlayer != null)
+			{
+				SetupMaterial();
+			}
+		}
+		
+		void OnDestroy()
+		{
+			// Destroy existing material
+			if (_material != null)
+			{
+#if UNITY_EDITOR
+				Material.DestroyImmediate(_material);
+#else
+				Material.Destroy(_material);
+#endif
+				_material = null;
+			}
+		}
+
+		private Shader GetRequiredShader()
+		{
+			Shader result = null;
+
+			if (result == null && _mediaPlayer.TextureProducer != null)
+			{
+				switch (_mediaPlayer.TextureProducer.GetTextureAlphaPacking())
+				{
+					case AlphaPacking.None:
+						break;
+					case AlphaPacking.LeftRight:
+					case AlphaPacking.TopBottom:
+						result = _shaderAlphaPacking;
+						break;
+				}
+			}
+
+#if UNITY_PLATFORM_SUPPORTS_LINEAR
+			if (result == null && _mediaPlayer.Info != null)
+			{
+				// If the player does support generating sRGB textures then we need to use a shader to convert them for display via IMGUI
+				if (QualitySettings.activeColorSpace == ColorSpace.Linear && !_mediaPlayer.Info.PlayerSupportsLinearColorSpace())
+				{
+					result = _shaderAlphaPacking;
+				}
+			}
+#endif
+			if (result == null && _mediaPlayer.TextureProducer != null)
+			{
+				if (_mediaPlayer.TextureProducer.GetTextureCount() == 2)
+				{
+					result = _shaderAlphaPacking;
+				}
+			}
+			return result;
+		}
+
+		private void SetupMaterial()
+		{
+			// Get required shader
+			Shader currentShader = null;
+			if (_material != null)
+			{
+				currentShader = _material.shader;
+			}
+			Shader nextShader = GetRequiredShader();
+
+			// If the shader requirement has changed
+			if (currentShader != nextShader)
+			{
+				// Destroy existing material
+				if (_material != null)
+				{
+#if UNITY_EDITOR
+					Material.DestroyImmediate(_material);
+#else
+					Material.Destroy(_material);
+#endif
+					_material = null;
+				}
+
+				// Create new material
+				if (nextShader != null)
+				{
+					_material = new Material(nextShader);
+				}
+			}
+		}
+
+#if UNITY_EDITOR
+		private void DrawArea()
+		{
+			Rect rect = GetAreaRect();
+			Rect uv = rect;
+			uv.x /= Screen.width;
+			uv.width /= Screen.width;
+			uv.y /= Screen.height;
+			uv.height /= Screen.height;
+			uv.width *= 16f;
+			uv.height *= 16f;
+			uv.x += 0.5f;
+			uv.y += 0.5f;
+			Texture2D icon = Resources.Load<Texture2D>("AVProVideoIcon");
+			GUI.depth = _depth;
+			GUI.color = _color;
+			GUI.DrawTextureWithTexCoords(rect, icon, uv);
+		}
+#endif
+
+		void OnGUI()
+		{
+#if UNITY_EDITOR
+			if (_showAreaInEditor && !Application.isPlaying)
+			{
+				DrawArea();
+				return;
+			}
+#endif
+
+			if (_mediaPlayer == null)
+			{
+				return;
+			}
+
+			Texture texture = null;
+			if (_showAreaInEditor)
+			{
+#if UNITY_EDITOR
+				texture = Texture2D.whiteTexture;
+#endif
+			}
+			texture = VideoRender.GetTexture(_mediaPlayer, 0);
+			if (_mediaPlayer.Info != null && !_mediaPlayer.Info.HasVideo())
+			{
+				texture = null;
+			}
+
+			if (texture != null)
+			{
+				bool isTextureVisible = (_color.a > 0f || !_allowTransparency);
+				if (isTextureVisible)
+				{
+					GUI.depth = _depth;
+					GUI.color = _color;
+
+					Rect rect = GetAreaRect();
+
+					// TODO: change this to a material-only path so we only have a single drawing path
+					if (_material != null)
+					{
+						// TODO: Only setup material when needed
+						VideoRender.SetupMaterialForMedia(_material, _mediaPlayer);
+
+						// NOTE: It seems that Graphics.DrawTexture() behaves differently than GUI.DrawTexture() when it comes to sRGB writing
+						// on newer versions of Unity (at least 2018.2.19 and above), so now we have to force the conversion to sRGB on writing
+						bool restoreSRGBWrite = false;
+#if UNITY_EDITOR_WIN || (!UNITY_EDITOR && UNITY_STANDALONE_WIN)
+						if (QualitySettings.activeColorSpace == ColorSpace.Linear && !GL.sRGBWrite)
+						{
+							restoreSRGBWrite = true;
+						}
+#endif
+						if (restoreSRGBWrite)
+						{
+							GL.sRGBWrite = true;
+						}
+
+						VideoRender.DrawTexture(rect, texture, _scaleMode, _mediaPlayer.TextureProducer.GetTextureAlphaPacking(), _mediaPlayer.TextureProducer.GetTexturePixelAspectRatio(), _material);
+						
+						if (restoreSRGBWrite)
+						{
+							GL.sRGBWrite = false;
+						}
+					}
+					else
+					{
+						bool requiresVerticalFlip = false;
+						if (_mediaPlayer.TextureProducer != null)
+						{
+							requiresVerticalFlip = _mediaPlayer.TextureProducer.RequiresVerticalFlip();
+						}
+						if (requiresVerticalFlip)
+						{
+							GUIUtility.ScaleAroundPivot(new Vector2(1f, -1f), new Vector2(0f, rect.y + (rect.height / 2f)));
+						}
+						#if UNITY_PLATFORM_SUPPORTS_VIDEOASPECTRATIO
+						float par = _mediaPlayer.TextureProducer.GetTexturePixelAspectRatio();
+						if (par > 0f)
+						{
+							if (par > 1f)
+							{
+								GUIUtility.ScaleAroundPivot(new Vector2(par, 1f), new Vector2(rect.x + (rect.width / 2f), rect.y + (rect.height / 2f)));
+							}
+							else
+							{
+								GUIUtility.ScaleAroundPivot(new Vector2(1f, 1f/par), new Vector2(rect.x + (rect.width / 2f), rect.y + (rect.height / 2f)));
+							}
+						}
+						#endif
+						GUI.DrawTexture(rect, texture, _scaleMode, _allowTransparency);
+					}
+				}
+			}
+		}
+
+		public Rect GetAreaRect()
+		{
+			Rect rect;
+			if (_isAreaFullScreen)
+			{
+				rect = new Rect(0.0f, 0.0f, Screen.width, Screen.height);
+			}
+			else
+			{
+				rect = new Rect(_areaX * (Screen.width - 1), _areaY * (Screen.height - 1), _areaWidth * Screen.width, _areaHeight * Screen.height);
+			}
+
+			return rect;
+		}
+	}
+}

+ 12 - 0
Assets/AVProVideo/Runtime/Scripts/Components/DisplayIMGUI.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 75f3b319d2d69934d8bf545ab45c918d
+timeCreated: 1544813301
+licenseType: Pro
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {fileID: 2800000, guid: bb83b41b53a59874692b83eab5873998, type: 3}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 1477 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer.cs

@@ -0,0 +1,1477 @@
+//#define AVPROVIDEO_BETA_SUPPORT_TIMESCALE		// BETA FEATURE: comment this in if you want to support frame stepping based on changes in Time.timeScale or Time.captureFramerate
+//#define AVPROVIDEO_FORCE_NULL_MEDIAPLAYER		// DEV FEATURE: comment this out to make all mediaplayers use the null mediaplayer
+//#define AVPROVIDEO_DISABLE_LOGGING			// DEV FEATURE: disables Debug.Log from AVPro Video
+#define AVPROVIDEO_SUPPORT_LIVEEDITMODE
+using UnityEngine;
+using UnityEngine.Serialization;
+using System.Collections;
+using System.Collections.Generic;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	/// <summary>
+	/// This is the primary AVPro Video component and handles all media loading,
+	/// seeking, information retrieving etc.  This component does not do any display
+	/// of the video.  Instead this is handled by other components such as
+	/// ApplyToMesh, ApplyToMaterial, DisplayIMGUI, DisplayUGUI.
+	/// </summary>
+#if AVPROVIDEO_SUPPORT_LIVEEDITMODE
+	[ExecuteInEditMode]
+#endif
+	[AddComponentMenu("AVPro Video/Media Player", -100)]
+	[HelpURL("https://www.renderheads.com/products/avpro-video/")]
+	public partial class MediaPlayer : MonoBehaviour
+	{
+		// These fields are just used to setup the default properties for a new video that is about to be loaded
+		// Once a video has been loaded you should use the interfaces exposed in the properties to
+		// change playback properties (eg volume, looping, mute)
+
+		// Media source
+
+		[SerializeField] MediaSource _mediaSource = MediaSource.Reference;
+		public MediaSource MediaSource { get { return _mediaSource; } internal set { _mediaSource = value; } }
+
+		[SerializeField] MediaReference _mediaReference = null;
+		public MediaReference MediaReference { get { return _mediaReference; } internal set { _mediaReference = value; } }
+
+		[SerializeField] MediaPath _mediaPath = new MediaPath();
+		public MediaPath MediaPath { get { return _mediaPath; } internal set { _mediaPath = value; } }
+
+		[SerializeField] MediaHints _fallbackMediaHints = MediaHints.Default;
+		public MediaHints FallbackMediaHints { get { return _fallbackMediaHints; } set { _fallbackMediaHints = value; } }
+
+		[FormerlySerializedAs("m_AutoOpen")]
+		[SerializeField] bool _autoOpen = true;
+		public bool AutoOpen { get { return _autoOpen; } set { _autoOpen = value; } }
+
+		[FormerlySerializedAs("m_AutoStart")]
+		[SerializeField] bool _autoPlayOnStart = true;
+		public bool AutoStart { get { return _autoPlayOnStart; } set { _autoPlayOnStart = value; } }
+
+		// Basic controls
+
+		[FormerlySerializedAs("m_Loop")]
+		[SerializeField] bool _loop = false;
+		public bool Loop
+		{
+			get
+			{
+				return (_controlInterface != null) ? _controlInterface.IsLooping() : _loop;
+			}
+			
+			set
+			{
+				_loop = value;
+				if (_controlInterface != null)
+					_controlInterface.SetLooping(_loop);
+			}
+		}
+
+		[FormerlySerializedAs("m_Volume")]
+		[Range(0.0f, 1.0f)]
+		[SerializeField] float _audioVolume = 1.0f;
+		public virtual float AudioVolume
+		{
+			get
+			{
+				return (_controlInterface != null) ? _controlInterface.GetVolume() : _audioVolume;
+			}
+			
+			set
+			{
+				_audioVolume = Mathf.Clamp01(value);
+				if (_controlInterface != null)
+					_controlInterface.SetVolume(_audioVolume);
+			}
+		}
+
+		[FormerlySerializedAs("m_Balance")]
+		[Range(-1.0f, 1.0f)]
+		[SerializeField] float _audioBalance = 0.0f;
+		public float AudioBalance
+		{
+			get
+			{
+				return (_controlInterface != null) ? _controlInterface.GetBalance() : _audioBalance;
+			}
+			
+			set
+			{
+				_audioBalance = Mathf.Clamp(value, -1f, 1f);
+				if (_controlInterface != null)
+					_controlInterface.SetBalance(_audioBalance);
+			}
+		}
+
+		[FormerlySerializedAs("m_Muted")]
+		[SerializeField] bool _audioMuted = false;
+		public virtual bool AudioMuted 
+		{
+			get
+			{
+				return (_controlInterface != null) ? _controlInterface.IsMuted() : _audioMuted;
+			}
+			
+			set
+			{
+				_audioMuted = value;
+				if (_controlInterface != null)
+				{
+					#if !UNITY_EDITOR
+						_controlInterface.MuteAudio(_audioMuted);
+					#else
+						_controlInterface.MuteAudio(_audioMuted || UnityEditor.EditorUtility.audioMasterMute);
+					#endif
+				}
+			}
+		}
+
+		private AudioSource _audioSource = null;
+		public AudioSource AudioSource { get { return _audioSource; } internal set { _audioSource = value; } }
+
+		[FormerlySerializedAs("m_PlaybackRate")]
+		[Range(-4.0f, 4.0f)]
+		[SerializeField] float _playbackRate = 1.0f;
+		public float PlaybackRate
+		{
+			get
+			{
+				return (_controlInterface != null) ? _controlInterface.GetPlaybackRate() : _playbackRate;
+			}
+			
+			set
+			{
+				_playbackRate = value;
+				if (_controlInterface != null)
+					_controlInterface.SetPlaybackRate(_playbackRate);
+			}
+		}
+
+		// Resampler
+
+		[FormerlySerializedAs("m_Resample")]
+		[SerializeField] bool _useResampler = false;
+		public bool UseResampler { get { return _useResampler; } set { _useResampler = value; } }
+
+		[FormerlySerializedAs("m_ResampleMode")]
+		[SerializeField] Resampler.ResampleMode _resampleMode = Resampler.ResampleMode.POINT;
+		public Resampler.ResampleMode ResampleMode { get { return _resampleMode; } set { _resampleMode = value; } }
+		
+		[FormerlySerializedAs("m_ResampleBufferSize")]
+		[Range(3, 10)]
+		[SerializeField] int _resampleBufferSize = 5;
+		public int ResampleBufferSize { get { return _resampleBufferSize; } set { _resampleBufferSize = value; } }
+
+		private Resampler _resampler = null;
+		public Resampler FrameResampler	{ get { return _resampler; } }
+
+		// Visual options
+
+		[FormerlySerializedAs("m_videoMapping")]
+		[SerializeField] VideoMapping _videoMapping = VideoMapping.Unknown;
+		public VideoMapping VideoLayoutMapping { get { return _videoMapping; } set { _videoMapping = value; } }
+
+		[FormerlySerializedAs("m_FilterMode")]
+		[SerializeField] FilterMode _textureFilterMode = FilterMode.Bilinear;
+		public FilterMode TextureFilterMode
+		{
+			get
+			{
+				if (_controlInterface != null)
+				{
+					FilterMode filterMode = FilterMode.Point;
+					TextureWrapMode textureWrapMode = TextureWrapMode.Repeat;
+					int anisoLevel = 0;
+					_controlInterface.GetTextureProperties(out filterMode, out textureWrapMode, out anisoLevel);
+					return filterMode;
+				}
+				else
+					return _textureFilterMode;
+			}
+			
+			set
+			{
+				_textureFilterMode = value;
+				if (_controlInterface != null)
+					_controlInterface.SetTextureProperties(_textureFilterMode, _textureWrapMode, _textureAnisoLevel);
+			}
+		}
+
+		[FormerlySerializedAs("m_WrapMode")]
+		[SerializeField] TextureWrapMode _textureWrapMode = TextureWrapMode.Clamp;
+		public TextureWrapMode TextureWrapMode
+		{
+			get
+			{
+				if (_controlInterface != null)
+				{
+					FilterMode filterMode = FilterMode.Point;
+					TextureWrapMode textureWrapMode = TextureWrapMode.Repeat;
+					int anisoLevel = 0;
+					_controlInterface.GetTextureProperties(out filterMode, out textureWrapMode, out anisoLevel);
+					return textureWrapMode;
+				}
+				else
+					return _textureWrapMode;
+			}
+			
+			set
+			{
+				_textureWrapMode = value;
+				if (_controlInterface != null)
+					_controlInterface.SetTextureProperties(_textureFilterMode, _textureWrapMode, _textureAnisoLevel);
+			}
+		}
+
+		[FormerlySerializedAs("m_AnisoLevel")]
+		[Range(0, 16)]
+		[SerializeField] int _textureAnisoLevel = 0;
+		public int TextureAnisoLevel
+		{
+			get
+			{
+				if (_controlInterface != null)
+				{
+					FilterMode filterMode = FilterMode.Point;
+					TextureWrapMode textureWrapMode = TextureWrapMode.Repeat;
+					int anisoLevel = 0;
+					_controlInterface.GetTextureProperties(out filterMode, out textureWrapMode, out anisoLevel);
+					return anisoLevel;
+				}
+				else
+					return _textureAnisoLevel;
+			}
+
+			set
+			{
+				_textureAnisoLevel = value;
+				if (_controlInterface != null)
+					_controlInterface.SetTextureProperties(_textureFilterMode, _textureWrapMode, _textureAnisoLevel);
+			}
+		}
+
+		[SerializeField] bool _useVideoResolve = false;
+		public bool UseVideoResolve { get { return _useVideoResolve; } set { _useVideoResolve = value; } }
+
+		[SerializeField] VideoResolveOptions _videoResolveOptions = VideoResolveOptions.Create();
+		public VideoResolveOptions VideoResolveOptions { get { return _videoResolveOptions; } set { _videoResolveOptions = value; } }
+
+#if AVPRO_FEATURE_VIDEORESOLVE
+		[SerializeField] VideoResolve _videoResolve = new VideoResolve();
+#endif
+		// Sideloaded subtitles
+
+		[FormerlySerializedAs("m_LoadSubtitles")]
+		[SerializeField] bool _sideloadSubtitles;
+		public bool SideloadSubtitles { get { return _sideloadSubtitles; } set { _sideloadSubtitles = value; } }
+
+		[SerializeField] MediaPath _subtitlePath;
+		public MediaPath SubtitlePath { get { return _subtitlePath; } set { _subtitlePath = value; } }
+
+		// Audio 360
+
+		[FormerlySerializedAs("m_AudioHeadTransform")]
+		[SerializeField] Transform _audioHeadTransform;
+		public Transform AudioHeadTransform { set { _audioHeadTransform = value; }	get { return _audioHeadTransform; } }
+
+		[FormerlySerializedAs("m_AudioFocusEnabled")]
+		[SerializeField] bool _audioFocusEnabled;
+		public bool AudioFocusEnabled { get { return _audioFocusEnabled; } set { _audioFocusEnabled = value; } }
+
+		[FormerlySerializedAs("m_AudioFocusTransform")]
+		[SerializeField] Transform _audioFocusTransform;
+		public Transform AudioFocusTransform { get { return _audioFocusTransform; } set { _audioFocusTransform = value; } }
+
+		[FormerlySerializedAs("m_AudioFocusWidthDegrees")]
+		[SerializeField, Range(40f, 120f)] float _audioFocusWidthDegrees = 90f;
+		public float AudioFocusWidthDegrees { get { return _audioFocusWidthDegrees; } set { _audioFocusWidthDegrees = value; } }
+
+		[FormerlySerializedAs("m_AudioFocusOffLevelDB")]
+		[SerializeField, Range(-24f, 0f)] float _audioFocusOffLevelDB = 0f;
+		public float AudioFocusOffLevelDB { get { return _audioFocusOffLevelDB; } set { _audioFocusOffLevelDB = value; } }
+
+		// Network
+
+		[SerializeField] HttpHeaderData _httpHeaders = new HttpHeaderData();
+		public HttpHeaderData HttpHeaders { get { return _httpHeaders; } set { _httpHeaders = value; } }
+
+		[SerializeField] KeyAuthData _keyAuth = new KeyAuthData();
+		public KeyAuthData KeyAuth { get { return _keyAuth; } set { _keyAuth = value; } }
+
+		// Events
+
+		[FormerlySerializedAs("m_events")]
+		[SerializeField] MediaPlayerEvent _events = null;
+		public MediaPlayerEvent Events
+		{
+			get
+			{
+				if (_events == null)
+				{
+					_events = new MediaPlayerEvent();
+				}
+				return _events;
+			}
+		}
+
+		[FormerlySerializedAs("m_eventMask")]
+		[SerializeField] int _eventMask = -1;
+		public int EventMask { get { return _eventMask; } set { _eventMask = value; } }
+
+		[SerializeField] bool _pauseMediaOnAppPause = true;
+		public bool PauseMediaOnAppPause { get { return _pauseMediaOnAppPause; } set { _pauseMediaOnAppPause = value; }	}
+
+		[SerializeField] bool _playMediaOnAppUnpause = true;
+		public bool PlayMediaOnAppUnpause { get { return _playMediaOnAppUnpause; } set { _playMediaOnAppUnpause = value; } }
+
+		// Misc options
+
+		[FormerlySerializedAs("m_Persistent")]
+		[SerializeField] bool _persistent = false;
+		public bool Persistent { get { return _persistent; } set { _persistent = value; } }
+
+		[FormerlySerializedAs("m_forceFileFormat")]
+		[SerializeField] FileFormat _forceFileFormat = FileFormat.Unknown;
+		public FileFormat ForceFileFormat { get { return _forceFileFormat; } set { _forceFileFormat = value; } }
+
+		// Interfaces
+
+		private BaseMediaPlayer _baseMediaPlayer;
+		private IMediaControl _controlInterface;
+		private ITextureProducer _textureInterface;
+		private IMediaInfo _infoInterface;
+		private IMediaPlayer _playerInterface;
+		private IMediaSubtitles _subtitlesInterface;
+		private IMediaCache _cacheInterface;
+		private IBufferedDisplay _bufferedDisplayInterface;
+		private IVideoTracks _videoTracksInterface;
+		private IAudioTracks _audioTracksInterface;
+		private ITextTracks _textTracksInterface;
+		private System.IDisposable _disposeInterface;
+
+		public virtual IMediaInfo Info { get { return _infoInterface; } }
+		public virtual IMediaControl Control { get { return _controlInterface; } }
+		public virtual IMediaPlayer Player { get { return _playerInterface; } }
+		public virtual ITextureProducer TextureProducer	{ get { return _textureInterface; }	}
+		public virtual IMediaSubtitles Subtitles {	get { return _subtitlesInterface; }	}
+		public virtual IVideoTracks VideoTracks {	get { return _videoTracksInterface; } }
+		public virtual IAudioTracks AudioTracks {	get { return _audioTracksInterface; } }
+		public virtual ITextTracks TextTracks {	get { return _textTracksInterface; } }
+		public virtual IMediaCache Cache { get { return _cacheInterface; } }
+		public virtual IBufferedDisplay BufferedDisplay { get { return _bufferedDisplayInterface; } }
+
+		// State
+		private bool _isMediaOpened = false;
+		public bool MediaOpened	{ get { return _isMediaOpened; }	}
+		private bool _autoPlayOnStartTriggered = false;
+		private bool _wasPlayingOnPause = false;
+		private Coroutine _renderingCoroutine = null;
+
+		// Global init
+		private static bool s_GlobalStartup = false;
+		private static bool s_TrialVersion = false;
+
+		// Subtitle state
+		private MediaPath _queueSubtitlePath;
+		private Coroutine _loadSubtitlesRoutine;
+
+		// Extract frame
+		private static Camera _dummyCamera = null;
+		public delegate void ProcessExtractedFrame(Texture2D extractedFrame);
+
+		/// <summary>
+		/// Methods
+		/// </summary>
+
+		#if UNITY_EDITOR
+		static MediaPlayer()
+		{
+			SetupEditorPlayPauseSupport();
+		}
+		#endif
+
+		void Awake()
+		{
+			if (_persistent)
+			{
+				// TODO: set "this.transform.root.gameObject" to also DontDestroyOnLoad?
+				DontDestroyOnLoad(this.gameObject);
+			}
+		}
+
+		protected void Initialise()
+		{
+			BaseMediaPlayer mediaPlayer = CreateMediaPlayer();
+			if (mediaPlayer != null)
+			{
+				// Set-up interface
+				_baseMediaPlayer = mediaPlayer;
+				_controlInterface = mediaPlayer;
+				_textureInterface = mediaPlayer;
+				_infoInterface = mediaPlayer;
+				_playerInterface = mediaPlayer;
+				_subtitlesInterface = mediaPlayer;
+				_videoTracksInterface = mediaPlayer;
+				_audioTracksInterface = mediaPlayer;
+				_textTracksInterface = mediaPlayer;
+				_disposeInterface = mediaPlayer;
+				_cacheInterface = mediaPlayer;
+				_bufferedDisplayInterface = mediaPlayer;
+
+				string nativePluginVersion = mediaPlayer.GetVersion();
+				string expectedNativePluginVersion = mediaPlayer.GetExpectedVersion();
+
+				// Check that the plugin version number is not too old
+				if (!nativePluginVersion.StartsWith(expectedNativePluginVersion))
+				{
+					Debug.LogError("[AVProVideo] Plugin version number " + nativePluginVersion + " doesn't match the expected version number " + expectedNativePluginVersion + ".  It looks like the plugin didn't upgrade correctly.  To resolve this please restart Unity and try to upgrade the package again.");
+				}
+
+				s_TrialVersion = nativePluginVersion.Contains("-trial");
+
+				if (!s_GlobalStartup)
+				{
+					Helper.LogInfo(string.Format("Initialising AVPro Video v{0} (native plugin v{1}) on {2}/{3} (MT {4}) on {5}", Helper.AVProVideoVersion, nativePluginVersion, SystemInfo.graphicsDeviceName, SystemInfo.graphicsDeviceVersion, SystemInfo.graphicsMultiThreaded, Application.platform));
+
+					#if AVPROVIDEO_BETA_SUPPORT_TIMESCALE
+					Debug.LogWarning("[AVProVideo] TimeScale support used.  This could affect performance when changing Time.timeScale or Time.captureFramerate.  This feature is useful for supporting video capture system that adjust time scale during capturing.");
+					#endif
+
+					s_GlobalStartup = true;
+				}
+			}
+		}
+
+		void Start()
+		{
+#if UNITY_WEBGL
+			_useResampler = false;
+#endif
+			if (_controlInterface == null)
+			{
+				if (Application.isPlaying)
+				{
+					Initialise();
+					if (_controlInterface != null)
+					{
+						if (_autoOpen)
+						{
+							OpenMedia(_autoPlayOnStart);
+
+							if (_sideloadSubtitles && _subtitlesInterface != null && _subtitlePath != null && !string.IsNullOrEmpty(_subtitlePath.Path))
+							{
+								EnableSubtitles(_subtitlePath);
+							}
+						}
+
+						StartRenderCoroutine();
+					}
+				}
+			}
+		}
+
+		public bool OpenMedia(MediaPath path, bool autoPlay = true)
+		{
+			return OpenMedia(path.PathType, path.Path, autoPlay);
+		}
+
+		public bool OpenMedia(MediaPathType pathType, string path, bool autoPlay = true)
+		{
+			_mediaSource = MediaSource.Path;
+			_mediaPath.Path = path;
+			_mediaPath.PathType = pathType;
+			
+			return OpenMedia(autoPlay);
+		}
+
+		public bool OpenMedia(MediaReference mediaReference, bool autoPlay = true)
+		{
+			_mediaSource = MediaSource.Reference;
+			_mediaReference = mediaReference;
+
+			return OpenMedia(autoPlay);
+		}
+
+		public bool OpenMedia(bool autoPlay = true)
+		{
+			_autoPlayOnStart = autoPlay;
+
+			if (_controlInterface == null)
+			{
+				//_autoOpen = false;		 // If OpenVideoFromFile() is called before Start() then set _autoOpen to false so that it doesn't load the video a second time during Start()
+				Initialise();
+			}
+
+			return InternalOpenMedia();
+		}
+
+		private bool InternalOpenMedia()
+		{
+			bool result = false;
+			// Open the video file
+			if (_controlInterface != null)
+			{
+				CloseMedia();
+
+				_isMediaOpened = true;
+				_autoPlayOnStartTriggered = !_autoPlayOnStart;
+				_finishedFrameOpenCheck = true;
+				long fileOffset = GetPlatformFileOffset();	// TODO: replace this with MediaReference
+
+				MediaPath mediaPath = null;
+				MediaHints mediaHints = _fallbackMediaHints;
+
+				if (_mediaSource == MediaSource.Reference)
+				{
+					if (_mediaReference != null)
+					{
+						mediaPath = _mediaReference.GetCurrentPlatformMediaReference().MediaPath;
+						mediaHints = _mediaReference.GetCurrentPlatformMediaReference().Hints;
+						if (string.IsNullOrEmpty(mediaPath.Path))
+						{
+							mediaPath = null;
+						}
+					}
+					else
+					{
+						Debug.LogError("[AVProVideo] No MediaReference specified", this);
+					}
+				}
+				else if (_mediaSource == MediaSource.Path)
+				{
+					if (!string.IsNullOrEmpty(_mediaPath.Path))
+					{
+						mediaPath = _mediaPath;
+					}
+					else
+					{
+						Debug.LogError("[AVProVideo] No file path specified", this);
+					}
+				}
+				
+				if (null != mediaPath)
+				{
+					string fullPath = mediaPath.GetResolvedFullPath();
+					string customHttpHeaders = null;
+
+					bool checkForFileExist = true;
+					bool isURL = fullPath.Contains("://");
+					if (isURL)
+					{
+						checkForFileExist = false;
+						customHttpHeaders = GetPlatformHttpHeadersAsString();
+					}
+#if (!UNITY_EDITOR && UNITY_ANDROID)
+					checkForFileExist = false;
+#endif
+					if (checkForFileExist && !System.IO.File.Exists(fullPath))
+					{
+						Debug.LogError("[AVProVideo] File not found: " + fullPath, this);
+					}
+					else
+					{
+						Helper.LogInfo(string.Format("Opening {0} (offset {1}) with API {2}", fullPath, fileOffset, GetPlatformVideoApiString()), this);
+
+#if UNITY_EDITOR_WIN || (!UNITY_EDITOR && UNITY_STANDALONE_WIN)
+						// NOTE: We don't need to call SetAudioChannelMode on Android,
+						// as it's set when the AndroidMediaPlayer object is created
+						if (_optionsWindows.audioOutput == Windows.AudioOutput.FacebookAudio360)
+						{
+							_controlInterface.SetAudioChannelMode(_optionsWindows.audio360ChannelMode);
+						}
+						else
+						{
+							_controlInterface.SetAudioChannelMode(Audio360ChannelMode.INVALID);
+						}
+#elif (!UNITY_EDITOR && UNITY_WSA_10_0)
+						if (_optionsWindowsUWP.audioOutput == WindowsUWP.AudioOutput.FacebookAudio360)
+						{
+							_controlInterface.SetAudioChannelMode(_optionsWindowsUWP.audio360ChannelMode);
+						}
+						else
+						{
+							_controlInterface.SetAudioChannelMode(Audio360ChannelMode.INVALID);
+						}
+#endif
+						PlatformOptions options = GetCurrentPlatformOptions();
+						bool startWithHighestBitrate = false;
+						if (options != null)
+						{
+							startWithHighestBitrate = options.StartWithHighestBandwidth();
+						}
+
+						SetLoadOptions();
+
+						if (!_controlInterface.OpenMedia(fullPath, fileOffset, customHttpHeaders, mediaHints, (int)_forceFileFormat, startWithHighestBitrate))
+						{
+							Debug.LogError("[AVProVideo] Failed to open " + fullPath, this);
+						}
+						else
+						{
+							SetPlaybackOptions();
+							result = true;
+							StartRenderCoroutine();
+						}
+					}
+				}
+				else
+				{
+					Debug.LogError("[AVProVideo] No file path specified", this);
+				}
+			}
+			return result;
+		}
+
+		private void SetLoadOptions()
+		{
+			// On some platforms we can update the loading options without having to recreate the player
+	#if !AVPROVIDEO_FORCE_NULL_MEDIAPLAYER
+		#if (UNITY_EDITOR_OSX && UNITY_IOS) || (!UNITY_EDITOR && UNITY_IOS)
+		#elif (UNITY_EDITOR_OSX && UNITY_TVOS) || (!UNITY_EDITOR && UNITY_TVOS)
+		#elif (UNITY_EDITOR_OSX || (!UNITY_EDITOR && UNITY_STANDALONE_OSX))
+		#elif (UNITY_EDITOR_WIN) || (!UNITY_EDITOR && UNITY_STANDALONE_WIN)
+		#elif (!UNITY_EDITOR && UNITY_WSA_10_0)
+		#elif (!UNITY_EDITOR && UNITY_ANDROID)
+		#elif (!UNITY_EDITOR && UNITY_WEBGL)
+			((WebGLMediaPlayer)_baseMediaPlayer).SetOptions(_optionsWebGL);
+		#endif
+	#endif
+
+			// Encryption support
+			PlatformOptions options = GetCurrentPlatformOptions();
+			if (options != null)
+			{
+				_controlInterface.SetKeyServerAuthToken(options.GetKeyServerAuthToken());
+				//_controlInterface.SetKeyServerURL(options.GetKeyServerURL());
+				_controlInterface.SetOverrideDecryptionKey(options.GetOverrideDecryptionKey());
+			}
+		}
+
+		private void SetPlaybackOptions()
+		{
+			// Set playback options
+			if (_controlInterface != null)
+			{
+				_controlInterface.SetLooping(_loop);
+				_controlInterface.SetPlaybackRate(_playbackRate);
+				_controlInterface.SetVolume(_audioVolume);
+				_controlInterface.SetBalance(_audioBalance);
+				#if !UNITY_EDITOR
+				_controlInterface.MuteAudio(_audioMuted);
+				#else
+				_controlInterface.MuteAudio(_audioMuted || UnityEditor.EditorUtility.audioMasterMute);
+				#endif
+				_controlInterface.SetTextureProperties(_textureFilterMode, _textureWrapMode, _textureAnisoLevel);
+			}
+		}
+
+		public void CloseMedia()
+		{
+			// Close the media file
+			if (_controlInterface != null)
+			{
+				if (_events != null && _isMediaOpened && _events.HasListeners() && IsHandleEvent(MediaPlayerEvent.EventType.Closing))
+				{
+					_events.Invoke(this, MediaPlayerEvent.EventType.Closing, ErrorCode.None);
+				}
+
+				_autoPlayOnStartTriggered = false;
+				_isMediaOpened = false;
+				ResetEvents();
+
+				if (_loadSubtitlesRoutine != null)
+				{
+					StopCoroutine(_loadSubtitlesRoutine);
+					_loadSubtitlesRoutine = null;
+				}
+
+				_controlInterface.CloseMedia();
+			}
+
+			if (_resampler != null)
+			{
+				_resampler.Reset();
+			}
+
+			StopRenderCoroutine();
+		}
+
+		public void RewindPrerollPause()
+		{
+			PlatformOptionsWindows.pauseOnPrerollComplete = true;
+			if (BufferedDisplay != null)
+			{
+				BufferedDisplay.SetBufferedDisplayOptions(true);
+			}
+			Rewind(false);
+			Play();
+		}
+
+		public virtual void Play()
+		{
+			if (_controlInterface != null && _controlInterface.CanPlay())
+			{
+				_controlInterface.Play();
+
+				// Mark this event as done because it's irrelevant once playback starts
+				_eventFired_ReadyToPlay = true;
+			}
+			else
+			{
+				// Can't play, perhaps it's still loading?  Queuing play using _autoPlayOnStart to play after loading
+				_autoPlayOnStart = true;
+				_autoPlayOnStartTriggered = false;
+			}
+		}
+
+		public virtual void Pause()
+		{
+			if (_controlInterface != null && _controlInterface.IsPlaying())
+			{
+				_controlInterface.Pause();
+			}
+			_wasPlayingOnPause = false;
+#if AVPROVIDEO_BETA_SUPPORT_TIMESCALE
+			_timeScaleIsControlling = false;
+#endif
+		}
+
+		public void Stop()
+		{
+			if (_controlInterface != null)
+			{
+				_controlInterface.Stop();
+			}
+#if AVPROVIDEO_BETA_SUPPORT_TIMESCALE
+			_timeScaleIsControlling = false;
+#endif
+		}
+
+		public void Rewind(bool pause)
+		{
+			if (_controlInterface != null)
+			{
+				if (pause)
+				{
+					Pause();
+				}
+				_controlInterface.Rewind();
+			}
+		}
+
+		public void SeekToLiveTime(double offset = 0.0)
+		{
+			if (_controlInterface != null)
+			{
+				double liveTime = _controlInterface.GetBufferedTimes().MaxTime;
+				if (liveTime > 0.0)
+				{
+					_controlInterface.Seek(liveTime - offset);
+				}
+			}
+		}
+
+#if UNITY_EDITOR && AVPROVIDEO_SUPPORT_LIVEEDITMODE
+		public bool EditorUpdate()
+		{
+			if (_playerInterface != null)
+			{
+				Update();
+				_playerInterface.Render();
+				return true;
+			}
+			return false;
+		}
+#endif
+		protected virtual void Update()
+		{
+			if (_controlInterface != null)
+			{
+				// Auto start the playback
+				if (_isMediaOpened && _autoPlayOnStart && !_autoPlayOnStartTriggered && _controlInterface.CanPlay())
+				{
+					_autoPlayOnStartTriggered = true;
+					Play();
+				}
+
+				if (Application.isPlaying)
+				{
+					if (_renderingCoroutine == null && _controlInterface.CanPlay())
+					{
+						StartRenderCoroutine();
+					}
+				}
+
+				if (_subtitlesInterface != null && _queueSubtitlePath != null && !string.IsNullOrEmpty(_queueSubtitlePath.Path))
+				{
+					EnableSubtitles(_queueSubtitlePath);
+					_queueSubtitlePath = null;
+				}
+
+#if AVPROVIDEO_BETA_SUPPORT_TIMESCALE
+				UpdateTimeScale();
+#endif
+
+				UpdateAudioHeadTransform();
+				UpdateAudioFocus();
+				
+				_playerInterface.Update();
+
+				// Render (done in co-routine)
+				//_playerInterface.Render();
+
+				UpdateErrors();
+				UpdateEvents();
+
+				_playerInterface.EndUpdate();
+			}
+
+#if UNITY_EDITOR
+			CheckEditorAudioMute();
+#endif
+		}
+
+		private void LateUpdate()
+		{
+			UpdateResampler();
+		}
+
+		private void UpdateResampler()
+		{
+#if !UNITY_WEBGL
+			if (_useResampler)
+			{
+				if (_resampler == null)
+				{
+					_resampler = new Resampler(this, gameObject.name, _resampleBufferSize, _resampleMode);
+				}
+			}
+#else
+			_useResampler = false;
+#endif
+
+			if (_resampler != null)
+			{
+				_resampler.Update();
+				_resampler.UpdateTimestamp();
+			}
+		}
+
+		void OnEnable()
+		{
+			if (_controlInterface != null && _wasPlayingOnPause)
+			{
+				_autoPlayOnStart = true;
+				_autoPlayOnStartTriggered = false;
+				_wasPlayingOnPause = false;
+			}
+
+			if(_playerInterface != null)
+			{
+				_playerInterface.OnEnable();
+				StartRenderCoroutine();
+			}
+		}
+
+		void OnDisable()
+		{
+			if (_controlInterface != null)
+			{
+				if (_controlInterface.IsPlaying())
+				{
+					Pause();
+					// Force an update to ensure the player state is synchronised with the plugin
+					Update();
+					// Needs to follow Pause() otherwise it will be reset.
+					_wasPlayingOnPause = true;
+				}
+			}
+
+			StopRenderCoroutine();
+		}
+
+		protected virtual void OnDestroy()
+		{
+			CloseMedia();
+
+			_baseMediaPlayer = null;
+			_controlInterface = null;
+			_textureInterface = null;
+			_infoInterface = null;
+			_playerInterface = null;
+			_subtitlesInterface = null;
+			_cacheInterface = null;
+			_bufferedDisplayInterface = null;
+			_videoTracksInterface = null;
+			_audioTracksInterface = null;
+			_textTracksInterface = null;
+
+			if (_disposeInterface != null)
+			{
+				_disposeInterface.Dispose();
+				_disposeInterface = null;
+			}
+
+			if (_resampler != null)
+			{
+				_resampler.Release();
+				_resampler = null;
+			}
+
+			// TODO: possible bug if MediaPlayers are created and destroyed manually (instantiated), OnApplicationQuit won't be called!
+		}
+
+		public void ForceDispose()
+		{
+			OnDisable();
+			OnDestroy();
+		}
+
+#if UNITY_EDITOR
+		public static void EditorAllPlayersDispose()
+		{
+			AllPlayersDispose();
+		}
+#endif
+
+		private static void AllPlayersDispose()
+		{
+			// Clean up any open media players
+			MediaPlayer[] players = Resources.FindObjectsOfTypeAll<MediaPlayer>();
+			if (players != null && players.Length > 0)
+			{
+				for (int i = 0; i < players.Length; i++)
+				{
+					players[i].ForceDispose();
+				}
+			}
+		}
+
+		void OnApplicationQuit()
+		{
+			if (s_GlobalStartup)
+			{
+				Helper.LogInfo("Shutdown");
+
+				AllPlayersDispose();
+
+#if UNITY_EDITOR
+	#if UNITY_EDITOR_WIN
+				WindowsMediaPlayer.DeinitPlatform();
+				WindowsRtMediaPlayer.DeinitPlatform();
+	#endif
+#else
+	#if (UNITY_STANDALONE_WIN)
+				WindowsMediaPlayer.DeinitPlatform();
+				WindowsRtMediaPlayer.DeinitPlatform();
+	#elif (UNITY_ANDROID)
+				AndroidMediaPlayer.DeinitPlatform();
+	#endif
+#endif
+				s_GlobalStartup = false;
+			}
+		}
+
+#region Rendering Coroutine
+
+		private void StartRenderCoroutine()
+		{
+			if (_renderingCoroutine == null)
+			{
+				// Use the method instead of the method name string to prevent garbage
+				_renderingCoroutine = StartCoroutine(FinalRenderCapture());
+			}
+		}
+
+		private void StopRenderCoroutine()
+		{
+			if (_renderingCoroutine != null)
+			{
+				StopCoroutine(_renderingCoroutine);
+				_renderingCoroutine = null;
+			}
+		}
+
+		private IEnumerator FinalRenderCapture()
+		{
+			// Preallocate the YieldInstruction to prevent garbage
+			YieldInstruction wait = new WaitForEndOfFrame();
+			while (Application.isPlaying)
+			{
+				// NOTE: in editor, if the game view isn't visible then WaitForEndOfFrame will never complete
+				yield return wait;
+
+				if (this.enabled)
+				{
+					if (_playerInterface != null)
+					{
+						_playerInterface.Render();
+					}
+				}
+			}
+		}
+#endregion // Rendering Coroutine
+
+#region Platform and Path
+		public static Platform GetPlatform()
+		{
+			Platform result = Platform.Unknown;
+
+			// Setup for running in the editor (Either OSX, Windows or Linux)
+#if UNITY_EDITOR
+#if (UNITY_EDITOR_OSX && UNITY_EDITOR_64)
+			result = Platform.MacOSX;
+#elif UNITY_EDITOR_WIN
+			result = Platform.Windows;
+#endif
+#else
+			// Setup for running builds
+#if (UNITY_STANDALONE_WIN)
+			result = Platform.Windows;
+#elif (UNITY_STANDALONE_OSX)
+			result = Platform.MacOSX;
+#elif (UNITY_IPHONE || UNITY_IOS)
+			result = Platform.iOS;
+#elif (UNITY_TVOS)
+			result = Platform.tvOS;
+#elif (UNITY_ANDROID)
+			result = Platform.Android;
+#elif (UNITY_WSA_10_0)
+			result = Platform.WindowsUWP;
+#elif (UNITY_WEBGL)
+			result = Platform.WebGL;
+#endif
+
+#endif
+			return result;
+		}
+
+		public PlatformOptions GetCurrentPlatformOptions()
+		{
+			PlatformOptions result = null;
+
+#if UNITY_EDITOR
+#if (UNITY_EDITOR_OSX && UNITY_EDITOR_64)
+			result = _optionsMacOSX;
+#elif UNITY_EDITOR_WIN
+			result = _optionsWindows;
+#endif
+#else
+	// Setup for running builds
+
+#if (UNITY_STANDALONE_WIN)
+			result = _optionsWindows;
+#elif (UNITY_STANDALONE_OSX)
+			result = _optionsMacOSX;
+#elif (UNITY_IPHONE || UNITY_IOS)
+			result = _optionsIOS;
+#elif (UNITY_TVOS)
+			result = _optionsTVOS;
+#elif (UNITY_ANDROID)
+			result = _optionsAndroid;
+#elif (UNITY_WSA_10_0)
+			result = _optionsWindowsUWP;
+#elif (UNITY_WEBGL)
+			result = _optionsWebGL;
+#endif
+
+#endif
+			return result;
+		}
+
+#if UNITY_EDITOR
+		public PlatformOptions GetPlatformOptions(Platform platform)
+		{
+			PlatformOptions result = null;
+
+			switch (platform)
+			{
+				case Platform.Windows:
+					result = _optionsWindows;
+					break;
+				case Platform.MacOSX:
+					result = _optionsMacOSX;
+					break;
+				case Platform.Android:
+					result = _optionsAndroid;
+					break;
+				case Platform.iOS:
+					result = _optionsIOS;
+					break;
+				case Platform.tvOS:
+					result = _optionsTVOS;
+					break;
+				case Platform.WindowsUWP:
+					result = _optionsWindowsUWP;
+					break;
+				case Platform.WebGL:
+					result = _optionsWebGL;
+					break;
+			}
+
+			return result;
+		}
+
+		public static string GetPlatformOptionsVariable(Platform platform)
+		{
+			string result = string.Empty;
+
+			switch (platform)
+			{
+				case Platform.Windows:
+					result = "_optionsWindows";
+					break;
+				case Platform.MacOSX:
+					result = "_optionsMacOSX";
+					break;
+				case Platform.iOS:
+					result = "_optionsIOS";
+					break;
+				case Platform.tvOS:
+					result = "_optionsTVOS";
+					break;
+				case Platform.Android:
+					result = "_optionsAndroid";
+					break;
+				case Platform.WindowsUWP:
+					result = "_optionsWindowsUWP";
+					break;
+				case Platform.WebGL:
+					result = "_optionsWebGL";
+					break;
+			}
+
+			return result;
+		}
+#endif
+
+		private string GetPlatformVideoApiString()
+		{
+			string result = string.Empty;
+#if UNITY_EDITOR
+	#if UNITY_EDITOR_OSX
+	#elif UNITY_EDITOR_WIN
+			result = _optionsWindows.videoApi.ToString();
+	#elif UNITY_EDITOR_LINUX
+	#endif
+#else
+	#if UNITY_STANDALONE_WIN
+			result = _optionsWindows.videoApi.ToString();
+	#elif UNITY_WSA_10_0
+			result = _optionsWindowsUWP.videoApi.ToString();
+	#elif UNITY_ANDROID
+			result = _optionsAndroid.videoApi.ToString();
+	#endif
+#endif
+			return result;
+		}
+
+		private long GetPlatformFileOffset()
+		{
+			long result = 0;
+#if UNITY_EDITOR
+	#if UNITY_EDITOR_OSX
+	#elif UNITY_EDITOR_WIN
+	#elif UNITY_EDITOR_LINUX
+	#endif
+#else
+	#if UNITY_ANDROID
+			result = _optionsAndroid.fileOffset;
+	#endif
+#endif
+			return result;
+		}
+
+		private string GetPlatformHttpHeadersAsString()
+		{
+			string result = null;
+
+#if UNITY_EDITOR
+	#if UNITY_EDITOR_OSX
+			result = _optionsMacOSX.httpHeaders.ToValidatedString();
+	#elif UNITY_EDITOR_WIN
+			result = _optionsWindows.httpHeaders.ToValidatedString();
+	#elif UNITY_EDITOR_LINUX
+	#endif
+#else
+	#if UNITY_STANDALONE_OSX
+			result = _optionsMacOSX.httpHeaders.ToValidatedString();
+	#elif UNITY_STANDALONE_WIN
+			result = _optionsWindows.httpHeaders.ToValidatedString();
+	#elif UNITY_WSA_10_0
+			result = _optionsWindowsUWP.httpHeaders.ToValidatedString();
+	#elif UNITY_IOS || UNITY_IPHONE
+			result = _optionsIOS.httpHeaders.ToValidatedString();
+	#elif UNITY_TVOS
+			result = _optionsTVOS.httpHeaders.ToValidatedString();
+	#elif UNITY_ANDROID
+			result = _optionsAndroid.httpHeaders.ToValidatedString();
+	#elif UNITY_WEBGL
+	#endif
+#endif
+
+			if (!string.IsNullOrEmpty(result))
+			{
+				result = result.Trim();
+			}
+			
+			string globalHeaders = _httpHeaders.ToValidatedString();
+			if (!string.IsNullOrEmpty(globalHeaders))
+			{
+				result += globalHeaders;
+				result = result.Trim();
+			}
+
+			return result;
+		}
+
+		private string GetResolvedFilePath(string filePath, MediaPathType fileLocation)
+		{
+			string result = string.Empty;
+
+			result = Helper.GetFilePath(filePath, fileLocation);
+
+			#if (UNITY_EDITOR_WIN || (!UNITY_EDITOR && UNITY_STANDALONE_WIN))
+			if (result.Length > 200 && !result.Contains("://"))
+			{
+				result = Helper.ConvertLongPathToShortDOS83Path(result);
+			}
+			#endif
+
+			return result;
+		}
+#endregion // Platform and Path
+
+#region Create MediaPlayers
+		#if (UNITY_EDITOR_WIN) || (!UNITY_EDITOR && UNITY_STANDALONE_WIN)
+		private static BaseMediaPlayer CreateMediaPlayer(OptionsWindows options)
+		{
+			BaseMediaPlayer result = null;
+			if (options.videoApi == Windows.VideoApi.WinRT)
+			{
+				if (WindowsRtMediaPlayer.InitialisePlatform())
+				{
+					result = new WindowsRtMediaPlayer(options);
+				}
+				else
+				{
+					Debug.LogWarning(string.Format("[AVProVideo] Failed to initialise WinRT API - platform {0} may not support it.  Trying another video API...", SystemInfo.operatingSystem));
+				}
+			}
+
+			if (result == null)
+			{
+				if (WindowsMediaPlayer.InitialisePlatform())
+				{
+					result = new WindowsMediaPlayer(options);
+				}
+			}
+			return result;
+		}
+		#endif
+
+		#if (!UNITY_EDITOR && UNITY_WSA_10_0)
+		private static BaseMediaPlayer CreateMediaPlayer(OptionsWindowsUWP options)
+		{
+			BaseMediaPlayer result = null;
+			if (options.videoApi == WindowsUWP.VideoApi.WinRT)
+			{
+				if (WindowsRtMediaPlayer.InitialisePlatform())
+				{
+					result = new WindowsRtMediaPlayer(options);
+				}
+				else
+				{
+					Debug.LogWarning(string.Format("[AVProVideo] Failed to initialise WinRT API - platform {0} may not support it.  Trying another video API...", SystemInfo.operatingSystem));
+				}
+			}
+
+			if (result == null)
+			{
+				if (WindowsMediaPlayer.InitialisePlatform())
+				{
+					result = new WindowsMediaPlayer(options);
+				}
+			}
+			return result;
+		}
+		#endif
+
+		#if (!UNITY_EDITOR && UNITY_ANDROID)
+		private static BaseMediaPlayer CreateMediaPlayer(OptionsAndroid options)
+		{
+			BaseMediaPlayer result = null;
+			// Initialise platform (also unpacks videos from StreamingAsset folder (inside a jar), to the persistent data path)
+			if (AndroidMediaPlayer.InitialisePlatform())
+			{
+				result = new AndroidMediaPlayer(options);
+			}
+			return result;
+		}
+		#endif
+
+		#if (UNITY_EDITOR_OSX) || (!UNITY_EDITOR && (UNITY_STANDALONE_OSX || UNITY_IPHONE || UNITY_IOS || UNITY_TVOS))
+		private static BaseMediaPlayer CreateMediaPlayer(OptionsApple options)
+		{
+			AppleMediaPlayer mediaPlayer = new AppleMediaPlayer(options);
+			return mediaPlayer;
+		}
+		#endif
+
+		#if (!UNITY_EDITOR && UNITY_WEBGL)
+		private static BaseMediaPlayer CreateMediaPlayer(OptionsWebGL options)
+		{
+			BaseMediaPlayer result = null;
+			if (WebGLMediaPlayer.InitialisePlatform())
+			{
+				result = new WebGLMediaPlayer(options);
+			}
+			return result;
+		}
+		#endif
+
+		private static BaseMediaPlayer CreateMediaPlayerNull()
+		{
+			return new NullMediaPlayer();
+		}
+
+		public virtual BaseMediaPlayer CreateMediaPlayer()
+		{
+			BaseMediaPlayer mediaPlayer = null;
+
+	#if !AVPROVIDEO_FORCE_NULL_MEDIAPLAYER
+		#if (UNITY_EDITOR_OSX && UNITY_IOS) || (!UNITY_EDITOR && UNITY_IOS)
+			mediaPlayer = CreateMediaPlayer(_optionsIOS);
+		#elif (UNITY_EDITOR_OSX && UNITY_TVOS) || (!UNITY_EDITOR && UNITY_TVOS)
+			mediaPlayer = CreateMediaPlayer(_optionsTVOS);
+		#elif (UNITY_EDITOR_OSX || (!UNITY_EDITOR && UNITY_STANDALONE_OSX))
+			mediaPlayer = CreateMediaPlayer(_optionsMacOSX);
+		#elif (UNITY_EDITOR_WIN) || (!UNITY_EDITOR && UNITY_STANDALONE_WIN)
+			mediaPlayer = CreateMediaPlayer(_optionsWindows);
+		#elif (!UNITY_EDITOR && UNITY_WSA_10_0)
+			mediaPlayer = CreateMediaPlayer(_optionsWindowsUWP);
+		#elif (!UNITY_EDITOR && UNITY_ANDROID)
+			mediaPlayer = CreateMediaPlayer(_optionsAndroid);
+		#elif (!UNITY_EDITOR && UNITY_WEBGL)
+			mediaPlayer = CreateMediaPlayer(_optionsWebGL);
+		#endif
+	#endif
+			// Fallback
+			if (mediaPlayer == null)
+			{
+				Debug.LogError(string.Format("[AVProVideo] Not supported on this platform {0} {1} {2} {3}.  Using null media player!", Application.platform, SystemInfo.deviceModel, SystemInfo.processorType, SystemInfo.operatingSystem));
+				mediaPlayer = CreateMediaPlayerNull();
+			}
+
+			return mediaPlayer;
+		}
+#endregion // Create MediaPlayers
+
+		private void UpdateAudioFocus()
+		{
+			// TODO: we could use gizmos to draw the focus area
+			_controlInterface.SetAudioFocusEnabled(_audioFocusEnabled);
+			_controlInterface.SetAudioFocusProperties(_audioFocusOffLevelDB, _audioFocusWidthDegrees);
+			_controlInterface.SetAudioFocusRotation(_audioFocusTransform == null ? Quaternion.identity : _audioFocusTransform.rotation);
+		}
+
+		private void UpdateAudioHeadTransform()
+		{
+			if (_audioHeadTransform != null)
+			{
+				_controlInterface.SetAudioHeadRotation(_audioHeadTransform.rotation);
+			}
+			else
+			{
+				_controlInterface.ResetAudioHeadRotation();
+			}
+		}
+
+		private void UpdateErrors()
+		{
+			ErrorCode errorCode = _controlInterface.GetLastError();
+			if (ErrorCode.None != errorCode)
+			{
+				Debug.LogError("[AVProVideo] Error: " + Helper.GetErrorMessage(errorCode));
+
+				// Display additional information for load failures
+				if (ErrorCode.LoadFailed == errorCode)
+				{
+					#if !UNITY_EDITOR && UNITY_ANDROID
+					// TODO: Update this to handle case where media is MediaReference
+					if (_mediaPath.Path.ToLower().Contains("http://"))
+					{
+						Debug.LogError("Android 8 and above require HTTPS by default, change to HTTPS or enable ClearText in the AndroidManifest.xml");
+					}
+					#endif
+				}
+
+				if (_events != null && _events.HasListeners() && IsHandleEvent(MediaPlayerEvent.EventType.Error))
+				{
+					_events.Invoke(this, MediaPlayerEvent.EventType.Error, errorCode);
+				}
+			}
+		}
+
+		public bool IsUsingAndroidOESPath()
+		{
+			// Android OES mode is not available in the trial
+			bool result = (PlatformOptionsAndroid.useFastOesPath && !s_TrialVersion);
+			#if (UNITY_EDITOR || !UNITY_ANDROID)
+			result = false;
+			#endif
+			return result;
+		}
+
+#region Save Frame To PNG
+#if UNITY_EDITOR || (!UNITY_EDITOR && (UNITY_STANDALONE_WIN || UNITY_STANDALONE_OSX))
+		[ContextMenu("Save Frame To PNG")]
+		public void SaveFrameToPng()
+		{
+			Texture2D frame = ExtractFrame(null);
+			if (frame != null)
+			{
+				byte[] imageBytes = frame.EncodeToPNG();
+				if (imageBytes != null)
+				{
+					string timecode = Mathf.FloorToInt((float)(Control.GetCurrentTime() * 1000.0)).ToString("D8");
+					System.IO.File.WriteAllBytes("frame-" + timecode + ".png", imageBytes);
+				}
+
+				Destroy(frame);
+			}
+		}
+		[ContextMenu("Save Frame To EXR")]
+		public void SaveFrameToExr()
+		{
+			Texture frame = (Texture)TextureProducer.GetTexture(0);
+			if (frame != null)
+			{
+				RenderTexture rt = new RenderTexture(frame.width, frame.height, 0, RenderTextureFormat.ARGBFloat, RenderTextureReadWrite.Linear);
+				rt.Create();
+				Graphics.Blit(frame, rt);
+				Texture2D frameRead = new Texture2D(frame.width, frame.height, TextureFormat.RGBAFloat, false, true);
+				RenderTexture.active = rt;
+				frameRead.ReadPixels(new Rect(0f, 0f, frame.width, frame.height), 0, 0, false);
+				frameRead.Apply(false, false);
+				RenderTexture.active = null;
+				byte[] imageBytes = frameRead.EncodeToEXR();
+				if (imageBytes != null)
+				{
+					string timecode = Mathf.FloorToInt((float)(Control.GetCurrentTime() * 1000.0)).ToString("D8");
+					System.IO.File.WriteAllBytes("frame-" + timecode + ".exr", imageBytes);
+				}
+
+				Destroy(frame);
+				Texture2D.Destroy(frameRead);
+				RenderTexture.Destroy(rt);
+			}
+		}
+#endif
+#endregion // Save Frame To PNG
+	}
+}

+ 12 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 638c870cac4da414fba921606d504407
+timeCreated: 1544813302
+licenseType: Pro
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {fileID: 2800000, guid: bb83b41b53a59874692b83eab5873998, type: 3}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 469 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayerSync.cs

@@ -0,0 +1,469 @@
+#if AVPROVIDEO_SUPPORT_BUFFERED_DISPLAY
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using RenderHeads.Media.AVProVideo;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo.Experimental
+{
+	/// <summary>
+	/// Syncronise multiple MediaPlayer components (currently Windows ONLY using Media Foundation ONLY)
+	/// This feature requires Ultra Edition
+	/// </summary>
+	[AddComponentMenu("AVPro Video/Media Player Sync (BETA)", -90)]
+	[HelpURL("https://www.renderheads.com/products/avpro-video/")]
+	public class MediaPlayerSync : MonoBehaviour
+	{
+		[SerializeField] MediaPlayer _masterPlayer = null;
+		[SerializeField] MediaPlayer[] _slavePlayers = null;
+		[SerializeField] bool _playOnStart = true;
+		[SerializeField] bool _waitAfterPreroll = false;
+		[SerializeField] bool _logSyncErrors = false;
+
+		public MediaPlayer MasterPlayer { get { return _masterPlayer; } set { _masterPlayer = value; } }
+		public MediaPlayer[] SlavePlayers { get { return _slavePlayers; } set { _slavePlayers = value; } }
+		public bool PlayOnStart { get { return _playOnStart; } set { _playOnStart = value; } }
+		public bool WaitAfterPreroll { get { return _waitAfterPreroll; } set { _waitAfterPreroll = value; } }
+		public bool LogSyncErrors { get { return _logSyncErrors; } set { _logSyncErrors = value; } }
+
+		private enum State
+		{
+			Idle,
+			Loading,
+			Prerolling,
+			Prerolled,
+			Playing,
+			Finished,
+		}
+
+		private State _state = State.Idle;
+
+		void Awake()
+		{
+#if (UNITY_EDITOR_WIN || (!UNITY_EDITOR && UNITY_STANDALONE_WIN))
+			SetupPlayers();
+#else
+			Debug.LogError("[AVProVideo] This component only works on the Windows platform");
+			this.enabled = false;
+#endif
+		}
+
+		void Start()
+		{
+			if (_playOnStart)
+			{
+				StartPlayback();
+				_state = State.Loading;
+				_playOnStart = false;
+			}
+		}
+
+		public void OpenMedia(string[] mediaPaths)
+		{
+			Debug.Assert(mediaPaths.Length == (_slavePlayers.Length + 1));
+
+			_masterPlayer.MediaSource = MediaSource.Path;
+			_masterPlayer.MediaPath = new MediaPath(mediaPaths[0], MediaPathType.AbsolutePathOrURL);
+
+			for (int i = 0; i < _slavePlayers.Length; i++)
+			{
+				_slavePlayers[i].MediaSource = MediaSource.Path;
+				_slavePlayers[i].MediaPath = new MediaPath(mediaPaths[i+1], MediaPathType.AbsolutePathOrURL);
+			}
+
+			StartPlayback();
+		}
+
+		/// <summary>
+		/// This is called when _autoPlay is false and once the MediaPlayers have had their source media set
+		/// </summary>
+		[ContextMenu("StartPlayback")]
+		public void StartPlayback()
+		{
+			SetupPlayers();
+
+			if (!IsPrerolled())
+			{
+				OpenMediaAll();
+				_state = State.Loading;
+			}
+			else
+			{
+				PlayAll();
+				_state = State.Playing;
+			}
+		}
+
+		public void Seek(double time, bool approximate = true)
+		{
+			if (approximate)
+			{
+				SeekFastAll(time);
+			}
+			else
+			{
+				SeekAll(time);
+			}
+
+			_state = State.Prerolling;
+		}
+
+		public bool IsPrerolled()
+		{
+			return (_state == State.Prerolled);
+		}
+
+		void SetupPlayers()
+		{
+			SetupPlayer(_masterPlayer);
+			for (int i = 0; i < _slavePlayers.Length; i++)
+			{
+				SetupPlayer(_slavePlayers[i]);
+			}
+		}
+
+		void SetupPlayer(MediaPlayer player)
+		{
+			bool isMaster = (player == _masterPlayer);
+			player.AutoOpen = false;
+			player.AutoStart = false;
+			player.AudioMuted = !isMaster;
+			player.PlatformOptionsWindows.videoApi = Windows.VideoApi.MediaFoundation;
+			player.PlatformOptionsWindows.useLowLatency = true;
+			player.PlatformOptionsWindows.pauseOnPrerollComplete = true;
+			player.PlatformOptionsWindows.bufferedFrameSelection = isMaster ? BufferedFrameSelectionMode.ElapsedTimeVsynced : BufferedFrameSelectionMode.FromExternalTime;
+		}
+
+		// NOTE: We check on LateUpdate() as MediaPlayer uses Update() to update state and we want to make sure all players have been updated
+		void LateUpdate()
+		{
+			if (_state == State.Idle)
+			{
+			}
+			if (_state == State.Loading)
+			{
+				UpdateLoading();
+			}
+			if (_state == State.Prerolling)
+			{
+				UpdatePrerolling();
+			}
+			if (_state == State.Prerolled)
+			{
+				/*if (Input.GetKeyDown(KeyCode.Alpha0))
+				{
+					StartPlayback();
+				}*/
+			}
+			if (_state == State.Playing)
+			{
+				UpdatePlaying();
+			}
+			if (_state == State.Finished)
+			{
+			}
+
+#if UNITY_EDITOR
+			if (Input.GetKeyDown(KeyCode.Alpha5))
+			{
+				Debug.Log("sleep");
+				System.Threading.Thread.Sleep(16);
+			}
+			
+			/*if (Input.GetKeyDown(KeyCode.Alpha1))
+			{
+				double time = Random.Range(0f, (float)_masterPlayer.Info.GetDuration());
+				Seek(time);
+			}
+
+			long gcMemory = System.GC.GetTotalMemory(false);
+			//Debug.Log("GC: " + (gcMemory / 1024) + " " + (gcMemory - lastGcMemory));
+			if ((gcMemory - lastGcMemory) < 0)
+			{
+				Debug.LogWarning("COLLECTION!!! " + (lastGcMemory - gcMemory));
+			}
+			lastGcMemory = gcMemory;*/
+#endif
+		}
+
+		//long lastGcMemory = 0;
+
+		void UpdateLoading()
+		{
+			// Finished loading?
+			if (IsAllVideosLoaded())
+			{
+				// Assign the master and slaves
+				_masterPlayer.BufferedDisplay.SetBufferedDisplayMode(BufferedFrameSelectionMode.ElapsedTimeVsynced);
+
+				IBufferedDisplay[] slaves = new IBufferedDisplay[_slavePlayers.Length];
+				for (int i = 0; i < _slavePlayers.Length; i++)
+				{
+					slaves[i] = _slavePlayers[i].BufferedDisplay;
+				}
+				_masterPlayer.BufferedDisplay.SetSlaves(slaves);
+
+				//System.Threading.Thread.Sleep(1250);
+
+				// Begin preroll
+				PlayAll();
+
+				_state = State.Prerolling;
+			}
+		}
+
+		void UpdatePrerolling()
+		{
+			if (IsAllVideosPaused())
+			{
+				//System.Threading.Thread.Sleep(250);
+
+				if (_waitAfterPreroll)
+				{
+					_state = State.Prerolled;
+				}
+				else
+				{
+					PlayAll();
+					_state = State.Playing;
+				}
+			}
+		}
+
+		void UpdatePlaying()
+		{
+			if (_masterPlayer.Control.IsPlaying())
+			{
+				if (_logSyncErrors)
+				{
+					CheckSync();
+					CheckSmoothness();
+				}
+
+				BufferedFramesState state = _masterPlayer.BufferedDisplay.GetBufferedFramesState();
+				if (state.bufferedFrameCount < 3)
+				{
+					//Debug.LogWarning("FORCE SLEEP");
+					System.Threading.Thread.Sleep(16);
+				}
+			}
+			else
+			{
+				// Pause slaves
+				for (int i = 0; i < _slavePlayers.Length; i++)
+				{
+					MediaPlayer slave = _slavePlayers[i];
+					slave.Pause();
+				}
+			}
+
+			// Finished?
+			if (IsPlaybackFinished(_masterPlayer))
+			{
+				_state = State.Finished;
+			}
+		}
+
+		private long _lastTimeStamp;
+		private int _sameFrameCount;
+
+		void CheckSmoothness()
+		{
+			long timeStamp = _masterPlayer.TextureProducer.GetTextureTimeStamp();
+			//int frameCount = _masterPlayer.TextureProducer.GetTextureFrameCount();
+			long frameDuration = (long)(10000000f / _masterPlayer.Info.GetVideoFrameRate());
+
+			long vsyncDuration = (long)((QualitySettings.vSyncCount * 10000000f) / (float)Screen.currentResolution.refreshRate);
+			float vsyncFrames = (float)vsyncDuration / frameDuration;
+
+			float fractionalFrames = vsyncFrames - Mathf.FloorToInt(vsyncFrames);
+
+			if (fractionalFrames == 0f)
+			{
+				if (QualitySettings.vSyncCount != 0)
+				{
+					if (!Mathf.Approximately(_sameFrameCount, vsyncFrames))
+					{
+						Debug.LogWarning("Frame " + timeStamp + " was shown for " + _sameFrameCount + " frames instead of expected " + vsyncFrames);
+					}
+				}
+			}
+
+			long d = (timeStamp - _lastTimeStamp);
+			if (d != 0)
+			{
+				long threshold = 10000;
+				if (d > frameDuration + threshold ||
+					d < frameDuration - threshold)
+				{
+					Debug.LogWarning("Possible frame skip, " + timeStamp + " " + d);
+				}
+
+				_sameFrameCount = 1;
+			}
+			else
+			{
+				_sameFrameCount++;
+			}
+
+			_lastTimeStamp = timeStamp;
+			//Debug.Log(frameDuration);
+		}
+
+		void CheckSync()
+		{
+			long timeStamp = _masterPlayer.TextureProducer.GetTextureTimeStamp();
+
+			bool inSync = true;
+			foreach (MediaPlayer slavePlayer in _slavePlayers)
+			{
+				if (slavePlayer.TextureProducer.GetTextureTimeStamp() != timeStamp)
+				{
+					inSync = false;
+					break;
+				}
+			}
+
+			if (!inSync)
+			{
+				LogSyncState();
+				Debug.LogWarning("OUT OF SYNC!!!!!!!");
+				//Debug.Break();
+			}
+			else
+			{
+				//LogSyncState();
+			}
+		}
+
+		void LogSyncState()
+		{
+			string text = "Time - Full,Free\t\tRange\n";
+			text += LogSyncState(_masterPlayer) + "\n";
+			foreach (MediaPlayer slavePlayer in _slavePlayers)
+			{
+				text += LogSyncState(slavePlayer) + "\n";
+			}
+			Debug.Log(text);
+		}
+
+		string LogSyncState(MediaPlayer player)
+		{
+			BufferedFramesState state = player.BufferedDisplay.GetBufferedFramesState();
+			long timeStamp = player.TextureProducer.GetTextureTimeStamp();
+			string result = string.Format("{4} - {2},{3}\t\t{0}-{1}   ({5})", state.minTimeStamp, state.maxTimeStamp, state.bufferedFrameCount, state.freeFrameCount, timeStamp, Time.deltaTime);
+			return result;
+		}
+
+		void OpenMediaAll()
+		{
+			_masterPlayer.OpenMedia(autoPlay:false);
+			for (int i = 0; i < _slavePlayers.Length; i++)
+			{
+				_slavePlayers[i].OpenMedia(autoPlay:false);
+			}
+		}
+
+		void PauseAll()
+		{
+			_masterPlayer.Pause();
+			for (int i = 0; i < _slavePlayers.Length; i++)
+			{
+				_slavePlayers[i].Pause();
+			}
+		}
+
+		void PlayAll()
+		{
+			_masterPlayer.Play();
+			for (int i = 0; i < _slavePlayers.Length; i++)
+			{
+				_slavePlayers[i].Play();
+			}
+		}
+
+		void SeekAll(double time)
+		{
+			 _masterPlayer.Control.Seek(time);
+			foreach (MediaPlayer player in _slavePlayers)
+			{
+				player.Control.Seek(time);
+			}
+		}
+
+		void SeekFastAll(double time)
+		{
+			_masterPlayer.Control.SeekFast(time);
+			foreach (MediaPlayer player in _slavePlayers)
+			{
+				player.Control.SeekFast(time);
+			}
+		}
+
+		bool IsAllVideosLoaded()
+		{
+			bool result = false;
+			if (IsVideoLoaded(_masterPlayer))
+			{
+				result = true;
+				for (int i = 0; i < _slavePlayers.Length; i++)
+				{
+					if (!IsVideoLoaded(_slavePlayers[i]))
+					{
+						result = false;
+						break;
+					}
+				}
+			}
+			return result;
+		}
+
+		bool IsAllVideosPaused()
+		{
+			bool result = false;
+			if (IsVideoPaused(_masterPlayer))
+			{
+				result = true;
+				for (int i = 0; i < _slavePlayers.Length; i++)
+				{
+					if (!IsVideoPaused(_slavePlayers[i]))
+					{
+						result = false;
+						break;
+					}
+				}
+			}
+			return result;
+		}
+		static bool IsPlaybackFinished(MediaPlayer player)
+		{
+			bool result = false;
+			if (player != null && player.Control != null)
+			{
+				if (player.Control.IsFinished())
+				{
+					BufferedFramesState state = player.BufferedDisplay.GetBufferedFramesState();
+					if (state.bufferedFrameCount == 0)
+					{
+						result = true;
+					}
+				}
+			}
+			return result;
+		}
+
+		static bool IsVideoLoaded(MediaPlayer player)
+		{
+			return (player != null && player.Control != null && player.Control.HasMetaData() && player.Control.CanPlay());
+		}
+		static bool IsVideoPaused(MediaPlayer player)
+		{
+			return (player != null && player.Control != null && player.Control.IsPaused());
+		}
+	}
+}
+#endif

+ 12 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayerSync.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 7b84192b70cb6c14ba22896883851268
+timeCreated: 1628084656
+licenseType: Pro
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {fileID: 2800000, guid: bb83b41b53a59874692b83eab5873998, type: 3}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 65 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_AppFocus.cs

@@ -0,0 +1,65 @@
+#if !(UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS || UNITY_TVOS)
+using UnityEngine;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	public partial class MediaPlayer : MonoBehaviour
+	{
+#region Application Focus and Pausing
+#if !UNITY_EDITOR
+		void OnApplicationFocus(bool focusStatus)
+		{
+#if !(UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN)
+//			Debug.Log("OnApplicationFocus: focusStatus: " + focusStatus);
+
+			if (focusStatus && (isActiveAndEnabled && enabled))
+			{
+				if (Control != null && _wasPlayingOnPause)
+				{
+					_wasPlayingOnPause = false;
+					Control.Play();
+
+					Helper.LogInfo("OnApplicationFocus: playing video again");
+				}
+			}
+#endif
+		}
+
+		void OnApplicationPause(bool pauseStatus)
+		{
+#if !(UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN)
+//			Debug.Log("OnApplicationPause: pauseStatus: " + pauseStatus);
+
+			if (pauseStatus)
+			{
+				if (_pauseMediaOnAppPause)
+				{
+					if (Control!= null && Control.IsPlaying())
+					{
+						_wasPlayingOnPause = true;
+#if !UNITY_IPHONE
+						Control.Pause();
+#endif
+						Helper.LogInfo("OnApplicationPause: pausing video");
+					}
+				}
+			}
+			else
+			{
+				if (_playMediaOnAppUnpause)
+				{
+					// Catch coming back from power off state when no lock screen
+					OnApplicationFocus(true);
+				}
+			}
+#endif
+		}
+#endif
+#endregion // Application Focus and Pausing
+	}
+}
+#endif

+ 12 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_AppFocus.cs.meta

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

+ 29 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_EditorMute.cs

@@ -0,0 +1,29 @@
+using UnityEngine;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	public partial class MediaPlayer : MonoBehaviour
+	{
+#region Audio Mute Support for Unity Editor
+#if UNITY_EDITOR
+		private bool _unityAudioMasterMute = false;
+		private void CheckEditorAudioMute()
+		{
+			// Detect a change
+			if (UnityEditor.EditorUtility.audioMasterMute != _unityAudioMasterMute)
+			{
+				if (_controlInterface != null)
+				{
+					_unityAudioMasterMute = UnityEditor.EditorUtility.audioMasterMute;
+					_controlInterface.MuteAudio(_audioMuted || _unityAudioMasterMute);
+				}
+			}
+		}
+#endif
+#endregion // Audio Mute Support for Unity Editor
+	}
+}

+ 12 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_EditorMute.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 16be519f584387149bd75947276c3a72
+timeCreated: 1544813302
+licenseType: Pro
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {fileID: 2800000, guid: bb83b41b53a59874692b83eab5873998, type: 3}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 83 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_EditorPlayPause.cs

@@ -0,0 +1,83 @@
+using UnityEngine;
+
+#if UNITY_EDITOR
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	public partial class MediaPlayer : MonoBehaviour
+	{
+#region Play/Pause Support for Unity Editor
+		// This code handles the pause/play buttons in the editor
+		private static void SetupEditorPlayPauseSupport()
+		{
+			#if UNITY_2017_2_OR_NEWER
+			UnityEditor.EditorApplication.pauseStateChanged -= OnUnityPauseModeChanged;
+			UnityEditor.EditorApplication.pauseStateChanged += OnUnityPauseModeChanged;
+			#else
+			UnityEditor.EditorApplication.playmodeStateChanged -= OnUnityPlayModeChanged;
+			UnityEditor.EditorApplication.playmodeStateChanged += OnUnityPlayModeChanged;
+			#endif
+		}
+
+		#if UNITY_2017_2_OR_NEWER
+		private static void OnUnityPauseModeChanged(UnityEditor.PauseState state)
+		{
+			OnUnityPlayModeChanged();
+		}
+		#endif
+
+		private static void OnUnityPlayModeChanged()
+		{
+			if (UnityEditor.EditorApplication.isPlaying)
+			{
+				bool isPaused = UnityEditor.EditorApplication.isPaused;
+				MediaPlayer[] players = Resources.FindObjectsOfTypeAll<MediaPlayer>();
+				foreach (MediaPlayer player in players)
+				{
+					if (isPaused)
+					{
+						player.EditorPause();
+					}
+					else
+					{
+						player.EditorUnpause();
+					}
+				}
+			}
+		}
+
+		private void EditorPause()
+		{
+			if (this.isActiveAndEnabled)
+			{
+				if (_controlInterface != null && _controlInterface.IsPlaying())
+				{
+					_wasPlayingOnPause = true;
+					_controlInterface.Pause();
+				}
+				StopRenderCoroutine();
+			}
+		}
+
+		private void EditorUnpause()
+		{
+			if (this.isActiveAndEnabled)
+			{
+				if (_controlInterface != null && _wasPlayingOnPause)
+				{
+					_autoPlayOnStart = true;
+					_wasPlayingOnPause = false;
+					_autoPlayOnStartTriggered = false;
+				}
+				StartRenderCoroutine();
+			}
+		}
+#endregion // Play/Pause Support for Unity Editor
+	}
+}
+
+#endif

+ 12 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_EditorPlayPause.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 083c5ace9dbfda84cb8b4afaa19bdcde
+timeCreated: 1544813302
+licenseType: Pro
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {fileID: 2800000, guid: bb83b41b53a59874692b83eab5873998, type: 3}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 269 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_Events.cs

@@ -0,0 +1,269 @@
+using UnityEngine;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	public partial class MediaPlayer : MonoBehaviour
+	{
+
+#region Events
+
+		// Event state
+		private bool _eventFired_MetaDataReady = false;
+		private bool _eventFired_ReadyToPlay = false;
+		private bool _eventFired_Started = false;
+		private bool _eventFired_FirstFrameReady = false;
+		private bool _eventFired_FinishedPlaying = false;
+		private bool _eventState_PlaybackBuffering = false;
+		private bool _eventState_PlaybackSeeking = false;
+		private bool _eventState_PlaybackStalled = false;
+		private int _eventState_PreviousWidth = 0;
+		private int _eventState_PreviousHeight = 0;
+		private int _previousSubtitleIndex = -1;
+		private bool _finishedFrameOpenCheck = false;
+
+		#if UNITY_EDITOR
+		public static MediaPlayerLoadEvent InternalMediaLoadedEvent = new MediaPlayerLoadEvent();
+		#endif
+
+		private void ResetEvents()
+		{
+			_eventFired_MetaDataReady = false;
+			_eventFired_ReadyToPlay = false;
+			_eventFired_Started = false;
+			_eventFired_FirstFrameReady = false;
+			_eventFired_FinishedPlaying = false;
+			_eventState_PlaybackBuffering = false;
+			_eventState_PlaybackSeeking = false;
+			_eventState_PlaybackStalled = false;
+			_eventState_PreviousWidth = 0;
+			_eventState_PreviousHeight = 0;
+			_previousSubtitleIndex = -1;
+			_finishedFrameOpenCheck = false;
+		}
+
+		private void UpdateEvents()
+		{
+			if (_events != null && _controlInterface != null && _events.HasListeners())
+			{
+				//NOTE: Fixes a bug where the event was being fired immediately, so when a file is opened, the finishedPlaying fired flag gets set but
+				//is then set to true immediately afterwards due to the returned value
+				_finishedFrameOpenCheck = false;
+				if (IsHandleEvent(MediaPlayerEvent.EventType.FinishedPlaying))
+				{
+					if (FireEventIfPossible(MediaPlayerEvent.EventType.FinishedPlaying, _eventFired_FinishedPlaying))
+					{
+						_eventFired_FinishedPlaying = !_finishedFrameOpenCheck;
+					}
+				}
+
+				// Reset some event states that can reset during playback
+				{
+					// Keep track of whether the Playing state has changed
+					if (_eventFired_Started && IsHandleEvent(MediaPlayerEvent.EventType.Started) &&
+						_controlInterface != null && !_controlInterface.IsPlaying() && !_controlInterface.IsSeeking())
+					{
+						// Playing has stopped
+						_eventFired_Started = false;
+					}
+
+					// NOTE: We check _controlInterface isn't null in case the scene is unloaded in response to the FinishedPlaying event
+					if (_eventFired_FinishedPlaying && IsHandleEvent(MediaPlayerEvent.EventType.FinishedPlaying) &&
+						_controlInterface != null && _controlInterface.IsPlaying() && !_controlInterface.IsFinished())
+					{
+						bool reset = true;
+#if UNITY_EDITOR_WIN || (!UNITY_EDITOR && (UNITY_STANDALONE_WIN || UNITY_WSA))
+						reset = false;
+						if (_infoInterface.HasVideo())
+						{
+							// Some streaming HLS/Dash content don't provide a frame rate
+							if (_infoInterface.GetVideoFrameRate() > 0f)
+							{
+								// Don't reset if within a frame of the end of the video, important for time > duration workaround
+								float secondsPerFrame = 1f / _infoInterface.GetVideoFrameRate();
+								if (_infoInterface.GetDuration() - _controlInterface.GetCurrentTime() > secondsPerFrame)
+								{
+									reset = true;
+								}
+							}
+							else
+							{
+								// Just check if we're not beyond the duration
+								if (_controlInterface.GetCurrentTime() < _infoInterface.GetDuration())
+								{
+									reset = true;
+								}
+							}
+						}
+						else
+						{
+							// For audio only media just check if we're not beyond the duration
+							if (_controlInterface.GetCurrentTime() < _infoInterface.GetDuration())
+							{
+								reset = true;
+							}
+						}
+#endif
+						if (reset)
+						{
+							//Debug.Log("Reset");
+							_eventFired_FinishedPlaying = false;
+						}
+					}
+				}
+
+				// Events that can only fire once
+				{
+					_eventFired_MetaDataReady = FireEventIfPossible(MediaPlayerEvent.EventType.MetaDataReady, _eventFired_MetaDataReady);
+					_eventFired_ReadyToPlay = FireEventIfPossible(MediaPlayerEvent.EventType.ReadyToPlay, _eventFired_ReadyToPlay);
+					_eventFired_Started = FireEventIfPossible(MediaPlayerEvent.EventType.Started, _eventFired_Started);
+					_eventFired_FirstFrameReady = FireEventIfPossible(MediaPlayerEvent.EventType.FirstFrameReady, _eventFired_FirstFrameReady);
+				}
+
+				// Events that can fire multiple times
+				{
+					// Subtitle changing
+					if (FireEventIfPossible(MediaPlayerEvent.EventType.SubtitleChange, false))
+					{
+						_previousSubtitleIndex = _subtitlesInterface.GetSubtitleIndex();
+					}
+
+					// Resolution changing
+					if (FireEventIfPossible(MediaPlayerEvent.EventType.ResolutionChanged, false))
+					{
+						_eventState_PreviousWidth = _infoInterface.GetVideoWidth();
+						_eventState_PreviousHeight = _infoInterface.GetVideoHeight();
+					}
+
+					// Stalling
+					if (IsHandleEvent(MediaPlayerEvent.EventType.Stalled))
+					{
+						bool newState = _infoInterface.IsPlaybackStalled();
+						if (newState != _eventState_PlaybackStalled)
+						{
+							_eventState_PlaybackStalled = newState;
+
+							var newEvent = _eventState_PlaybackStalled ? MediaPlayerEvent.EventType.Stalled : MediaPlayerEvent.EventType.Unstalled;
+							FireEventIfPossible(newEvent, false);
+						}
+					}
+					// Seeking
+					if (IsHandleEvent(MediaPlayerEvent.EventType.StartedSeeking))
+					{
+						bool newState = _controlInterface.IsSeeking();
+						if (newState != _eventState_PlaybackSeeking)
+						{
+							_eventState_PlaybackSeeking = newState;
+
+							var newEvent = _eventState_PlaybackSeeking ? MediaPlayerEvent.EventType.StartedSeeking : MediaPlayerEvent.EventType.FinishedSeeking;
+							FireEventIfPossible(newEvent, false);
+						}
+					}
+					// Buffering
+					if (IsHandleEvent(MediaPlayerEvent.EventType.StartedBuffering))
+					{
+						bool newState = _controlInterface.IsBuffering();
+						if (newState != _eventState_PlaybackBuffering)
+						{
+							_eventState_PlaybackBuffering = newState;
+
+							var newEvent = _eventState_PlaybackBuffering ? MediaPlayerEvent.EventType.StartedBuffering : MediaPlayerEvent.EventType.FinishedBuffering;
+							FireEventIfPossible(newEvent, false);
+						}
+					}
+				}
+			}
+		}
+
+		protected bool IsHandleEvent(MediaPlayerEvent.EventType eventType)
+		{
+			return ((uint)_eventMask & (1 << (int)eventType)) != 0;
+		}
+
+		private bool FireEventIfPossible(MediaPlayerEvent.EventType eventType, bool hasFired)
+		{
+			if (CanFireEvent(eventType, hasFired))
+			{
+				#if UNITY_EDITOR
+				// Special internal global event, called when media is loaded
+				// Currently used by the RecentItem class
+				if (eventType == MediaPlayerEvent.EventType.Started)
+				{
+					string fullPath = GetResolvedFilePath(_mediaPath.Path, _mediaPath.PathType);
+					InternalMediaLoadedEvent.Invoke(fullPath);
+				}
+				#endif
+
+				hasFired = true;
+				_events.Invoke(this, eventType, ErrorCode.None);
+			}
+			return hasFired;
+		}
+
+		private bool CanFireEvent(MediaPlayerEvent.EventType et, bool hasFired)
+		{
+			bool result = false;
+			if (_events != null && _controlInterface != null && !hasFired && IsHandleEvent(et))
+			{
+				switch (et)
+				{
+					case MediaPlayerEvent.EventType.FinishedPlaying:
+						result = (!_controlInterface.IsLooping() && _controlInterface.CanPlay() && _controlInterface.IsFinished());
+						break;
+					case MediaPlayerEvent.EventType.MetaDataReady:
+						result = (_controlInterface.HasMetaData());
+						break;
+					case MediaPlayerEvent.EventType.FirstFrameReady:
+						// [MOZ 20/1/21] Removed HasMetaData check as preventing the event from being triggered on (i|mac|tv)OS
+						result = (_textureInterface != null && _controlInterface.CanPlay() /*&& _controlInterface.HasMetaData()*/ && _textureInterface.GetTextureFrameCount() > 0);
+						break;
+					case MediaPlayerEvent.EventType.ReadyToPlay:
+						result = (!_controlInterface.IsPlaying() && _controlInterface.CanPlay() && !_autoPlayOnStart);
+						break;
+					case MediaPlayerEvent.EventType.Started:
+						result = (_controlInterface.IsPlaying());
+						break;
+					case MediaPlayerEvent.EventType.SubtitleChange:
+					{
+						result = (_previousSubtitleIndex != _subtitlesInterface.GetSubtitleIndex());
+						if (!result)
+						{
+							result = _baseMediaPlayer.InternalIsChangedTextCue();
+						}
+						break;
+					}
+					case MediaPlayerEvent.EventType.Stalled:
+						result = _infoInterface.IsPlaybackStalled();
+						break;
+					case MediaPlayerEvent.EventType.Unstalled:
+						result = !_infoInterface.IsPlaybackStalled();
+						break;
+					case MediaPlayerEvent.EventType.StartedSeeking:
+						result = _controlInterface.IsSeeking();
+						break;
+					case MediaPlayerEvent.EventType.FinishedSeeking:
+						result = !_controlInterface.IsSeeking();
+						break;
+					case MediaPlayerEvent.EventType.StartedBuffering:
+						result = _controlInterface.IsBuffering();
+						break;
+					case MediaPlayerEvent.EventType.FinishedBuffering:
+						result = !_controlInterface.IsBuffering();
+						break;
+					case MediaPlayerEvent.EventType.ResolutionChanged:
+						result = (_infoInterface != null && (_eventState_PreviousWidth != _infoInterface.GetVideoWidth() || _eventState_PreviousHeight != _infoInterface.GetVideoHeight()));
+						break;
+					default:
+						Debug.LogWarning("[AVProVideo] Unhandled event type");
+						break;
+				}
+			}
+			return result;
+		}
+#endregion // Events
+
+	}
+}

+ 12 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_Events.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 6be886b3f1f953843bda70e505701ee3
+timeCreated: 1544813302
+licenseType: Pro
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {fileID: 2800000, guid: bb83b41b53a59874692b83eab5873998, type: 3}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 211 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_ExtractFrame.cs

@@ -0,0 +1,211 @@
+using UnityEngine;
+using System.Collections;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	public partial class MediaPlayer : MonoBehaviour
+	{
+#region Extract Frame
+
+		private bool ForceWaitForNewFrame(int lastFrameCount, float timeoutMs)
+		{
+			bool result = false;
+			// Wait for the frame to change, or timeout to happen (for the case that there is no new frame for this time)
+			System.DateTime startTime = System.DateTime.Now;
+			int iterationCount = 0;
+			while (Control != null && (System.DateTime.Now - startTime).TotalMilliseconds < (double)timeoutMs)
+			{
+				_playerInterface.Update();
+
+				// TODO: check if Seeking has completed!  Then we don't have to wait
+
+				// If frame has changed we can continue
+				// NOTE: this will never happen because GL.IssuePlugin.Event is never called in this loop
+				if (lastFrameCount != TextureProducer.GetTextureFrameCount())
+				{
+					result = true;
+					break;
+				}
+
+				iterationCount++;
+
+				// NOTE: we tried to add Sleep for 1ms but it was very slow, so switched to this time based method which burns more CPU but about double the speed
+				// NOTE: had to add the Sleep back in as after too many iterations (over 1000000) of GL.IssuePluginEvent Unity seems to lock up
+				// NOTE: seems that GL.IssuePluginEvent can't be called if we're stuck in a while loop and they just stack up
+				//System.Threading.Thread.Sleep(0);
+			}
+
+			_playerInterface.Render();
+
+			return result;
+		}
+		
+		/// <summary>
+		/// Create or return (if cached) a camera that is inactive and renders nothing
+		/// This camera is used to call .Render() on which causes the render thread to run
+		/// This is useful for forcing GL.IssuePluginEvent() to run and is used for
+		/// wait for frames to render for ExtractFrame() and UpdateTimeScale()
+		/// </summary>
+		private static Camera GetDummyCamera()
+		{
+			if (_dummyCamera == null)
+			{
+				const string goName = "AVPro Video Dummy Camera";
+				GameObject go = GameObject.Find(goName);
+				if (go == null)
+				{
+					go = new GameObject(goName);
+					go.hideFlags = HideFlags.HideInHierarchy | HideFlags.DontSave;
+					go.SetActive(false);
+					Object.DontDestroyOnLoad(go);
+
+					_dummyCamera = go.AddComponent<Camera>();
+					_dummyCamera.hideFlags = HideFlags.HideInInspector | HideFlags.DontSave;
+					_dummyCamera.cullingMask = 0;
+					_dummyCamera.clearFlags = CameraClearFlags.Nothing;
+					_dummyCamera.enabled = false;
+				}
+				else
+				{
+					_dummyCamera = go.GetComponent<Camera>();
+				}
+			}
+			//Debug.Assert(_dummyCamera != null);
+			return _dummyCamera;
+		}
+
+		private IEnumerator ExtractFrameCoroutine(Texture2D target, ProcessExtractedFrame callback, double timeSeconds = -1.0, bool accurateSeek = true, int timeoutMs = 1000, int timeThresholdMs = 100)
+		{
+#if (!UNITY_EDITOR && UNITY_ANDROID) || UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN || UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX || UNITY_IOS || UNITY_TVOS
+			Texture2D result = target;
+
+			Texture frame = null;
+
+			if (_controlInterface != null)
+			{
+				if (timeSeconds >= 0f)
+				{
+					Pause();
+
+					// If the right frame is already available (or close enough) just grab it
+					if (TextureProducer.GetTexture() != null && (System.Math.Abs(_controlInterface.GetCurrentTime() - timeSeconds) < (timeThresholdMs / 1000.0)))
+					{
+						frame = TextureProducer.GetTexture();
+					}
+					else
+					{
+						int preSeekFrameCount = _textureInterface.GetTextureFrameCount();
+
+						// Seek to the frame
+						if (accurateSeek)
+						{
+							_controlInterface.Seek(timeSeconds);
+						}
+						else
+						{
+							_controlInterface.SeekFast(timeSeconds);
+						}
+
+						// Wait for the new frame to arrive
+						if (!_controlInterface.WaitForNextFrame(GetDummyCamera(), preSeekFrameCount))
+						{
+							// If WaitForNextFrame fails (e.g. in android single threaded), we run the below code to asynchronously wait for the frame
+							int currFc = TextureProducer.GetTextureFrameCount();
+							int iterations = 0;
+							int maxIterations = 50;
+
+							//+1 as often there will be an extra frame produced after pause (so we need to wait for the second frame instead)
+							while((currFc + 1) >= TextureProducer.GetTextureFrameCount() && iterations++ < maxIterations)
+							{
+								yield return null;
+							}
+						}
+						frame = TextureProducer.GetTexture();
+					}
+				}
+				else
+				{
+					frame = TextureProducer.GetTexture();
+				}
+			}
+			if (frame != null)
+			{
+				result = Helper.GetReadableTexture(frame, TextureProducer.RequiresVerticalFlip(), Helper.GetOrientation(Info.GetTextureTransform()), target);
+			}
+#else
+			Texture2D result = ExtractFrame(target, timeSeconds, accurateSeek, timeoutMs, timeThresholdMs);
+#endif
+			callback(result);
+
+			yield return null;
+		}
+
+		public void ExtractFrameAsync(Texture2D target, ProcessExtractedFrame callback, double timeSeconds = -1.0, bool accurateSeek = true, int timeoutMs = 1000, int timeThresholdMs = 100)
+		{
+			StartCoroutine(ExtractFrameCoroutine(target, callback, timeSeconds, accurateSeek, timeoutMs, timeThresholdMs));
+		}
+
+		// "target" can be null or you can pass in an existing texture.
+		public Texture2D ExtractFrame(Texture2D target, double timeSeconds = -1.0, bool accurateSeek = true, int timeoutMs = 1000, int timeThresholdMs = 100)
+		{
+			Texture2D result = target;
+
+			// Extract frames returns the internal frame of the video player
+			Texture frame = ExtractFrame(timeSeconds, accurateSeek, timeoutMs, timeThresholdMs);
+			if (frame != null)
+			{
+				result = Helper.GetReadableTexture(frame, TextureProducer.RequiresVerticalFlip(), Helper.GetOrientation(Info.GetTextureTransform()), target);
+			}
+
+			return result;
+		}
+
+		private Texture ExtractFrame(double timeSeconds = -1.0, bool accurateSeek = true, int timeoutMs = 1000, int timeThresholdMs = 100)
+		{
+			Texture result = null;
+
+			if (_controlInterface != null)
+			{
+				if (timeSeconds >= 0f)
+				{
+					Pause();
+
+					// If the right frame is already available (or close enough) just grab it
+					if (TextureProducer.GetTexture() != null && (System.Math.Abs(_controlInterface.GetCurrentTime() - timeSeconds) < (timeThresholdMs / 1000.0)))
+					{
+						result = TextureProducer.GetTexture();
+					}
+					else
+					{
+						// Store frame count before seek
+						int frameCount = TextureProducer.GetTextureFrameCount();
+
+						// Seek to the frame
+						if (accurateSeek)
+						{
+							_controlInterface.Seek(timeSeconds);
+						}
+						else
+						{
+							_controlInterface.SeekFast(timeSeconds);
+						}
+
+						// Wait for frame to change
+						ForceWaitForNewFrame(frameCount, timeoutMs);
+						result = TextureProducer.GetTexture();
+					}
+				}
+				else
+				{
+					result = TextureProducer.GetTexture();
+				}
+			}
+			return result;
+		}
+#endregion // Extract Frame
+	}
+}

+ 12 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_ExtractFrame.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 810d3ce69a3b01f409c733c7cfbd119c
+timeCreated: 1544813302
+licenseType: Pro
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {fileID: 2800000, guid: bb83b41b53a59874692b83eab5873998, type: 3}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 129 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_OpenBuffer.cs

@@ -0,0 +1,129 @@
+using UnityEngine;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	public partial class MediaPlayer : MonoBehaviour
+	{
+		public bool OpenMediaFromBuffer(byte[] buffer, bool autoPlay = true)
+		{
+			_mediaPath = new MediaPath("buffer", MediaPathType.AbsolutePathOrURL);
+			_autoPlayOnStart = autoPlay;
+
+			if (_controlInterface == null)
+			{
+				Initialise();
+			}
+
+			return OpenMediaFromBufferInternal(buffer);
+		}
+
+		public bool StartOpenChunkedMediaFromBuffer(ulong length, bool autoPlay = true)
+		{
+			_mediaPath = new MediaPath("buffer", MediaPathType.AbsolutePathOrURL);
+			_autoPlayOnStart = autoPlay;
+
+			if (_controlInterface == null)
+			{
+				Initialise();
+			}
+
+			return StartOpenMediaFromBufferInternal(length);
+		}
+
+		public bool AddChunkToVideoBuffer(byte[] chunk, ulong offset, ulong chunkSize)
+		{
+			return AddChunkToBufferInternal(chunk, offset, chunkSize);
+		}
+
+		public bool EndOpenChunkedVideoFromBuffer()
+		{
+			return EndOpenMediaFromBufferInternal();
+		}
+
+		private bool OpenMediaFromBufferInternal(byte[] buffer)
+		{
+			bool result = false;
+			// Open the video file
+			if (_controlInterface != null)
+			{
+				CloseMedia();
+
+				_isMediaOpened = true;
+				_autoPlayOnStartTriggered = !_autoPlayOnStart;
+
+				Helper.LogInfo("Opening buffer of length " + buffer.Length, this);
+
+				if (!_controlInterface.OpenMediaFromBuffer(buffer))
+				{
+					Debug.LogError("[AVProVideo] Failed to open buffer", this);
+					if (GetCurrentPlatformOptions() != PlatformOptionsWindows || PlatformOptionsWindows.videoApi != Windows.VideoApi.DirectShow)
+					{
+						Debug.LogError("[AVProVideo] Loading from buffer is currently only supported in Windows when using the DirectShow API");
+					}
+				}
+				else
+				{
+					SetPlaybackOptions();
+					result = true;
+					StartRenderCoroutine();
+				}
+			}
+			return result;
+		}
+
+		private bool StartOpenMediaFromBufferInternal(ulong length)
+		{
+			bool result = false;
+			// Open the video file
+			if (_controlInterface != null)
+			{
+				CloseMedia();
+
+				_isMediaOpened = true;
+				_autoPlayOnStartTriggered = !_autoPlayOnStart;
+
+				Helper.LogInfo("Starting Opening buffer of length " + length, this);
+
+				if (!_controlInterface.StartOpenMediaFromBuffer(length))
+				{
+					Debug.LogError("[AVProVideo] Failed to start open video from buffer", this);
+					if (GetCurrentPlatformOptions() != PlatformOptionsWindows || PlatformOptionsWindows.videoApi != Windows.VideoApi.DirectShow)
+					{
+						Debug.LogError("[AVProVideo] Loading from buffer is currently only supported in Windows when using the DirectShow API");
+					}
+				}
+				else
+				{
+					SetPlaybackOptions();
+					result = true;
+					StartRenderCoroutine();
+				}
+			}
+			return result;
+		}
+
+		private bool AddChunkToBufferInternal(byte[] chunk, ulong offset, ulong chunkSize)
+		{
+			if (Control != null)
+			{
+				return Control.AddChunkToMediaBuffer(chunk, offset, chunkSize);
+			}
+
+			return false;
+		}
+
+		private bool EndOpenMediaFromBufferInternal()
+		{
+			if (Control != null)
+			{
+				return Control.EndOpenMediaFromBuffer();
+			}
+
+			return false;
+		}
+	}
+}

+ 12 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_OpenBuffer.cs.meta

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

+ 60 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_OpenStream.cs

@@ -0,0 +1,60 @@
+using UnityEngine;
+#if NETFX_CORE
+using Windows.Storage.Streams;
+#endif
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	public partial class MediaPlayer : MonoBehaviour
+	{
+
+#if NETFX_CORE
+		public bool OpenVideoFromStream(IRandomAccessStream ras, string path, bool autoPlay = true)
+		{
+			_videoLocation = FileLocation.AbsolutePathOrURL;
+			_videoPath = path;
+			_autoPlayOnStart = autoPlay;
+
+			if (_controlInterface == null)
+			{
+				Initialise();
+			}
+
+			return OpenVideoFromStream(ras);
+		}
+
+		private bool OpenVideoFromStream(IRandomAccessStream ras)
+		{
+			bool result = false;
+			// Open the video file
+			if (_controlInterface != null)
+			{
+				CloseVideo();
+
+				_isVideoOpened = true;
+				_autoPlayOnStartTriggered = !_autoPlayOnStart;
+
+				// Potentially override the file location
+				long fileOffset = GetPlatformFileOffset();
+
+				if (!Control.OpenVideoFromFile(ras, _videoPath, fileOffset, null, _manuallySetAudioSourceProperties ? _sourceAudioSampleRate : 0,
+					_manuallySetAudioSourceProperties ? _sourceAudioChannels : 0))
+				{
+					Debug.LogError("[AVProVideo] Failed to open " + _videoPath, this);
+				}
+				else
+				{
+					SetPlaybackOptions();
+					result = true;
+					StartRenderCoroutine();
+				}
+			}
+			return result;
+		}
+#endif
+	}
+}

+ 12 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_OpenStream.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 4e6c8c5399247d0478ed7ecf17b7d87f
+timeCreated: 1544813302
+licenseType: Pro
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {fileID: 2800000, guid: bb83b41b53a59874692b83eab5873998, type: 3}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 716 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_PlatformOptions.cs

@@ -0,0 +1,716 @@
+using UnityEngine;
+using System;
+using System.Collections.Generic;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	public partial class MediaPlayer : MonoBehaviour
+	{
+#region PlatformOptions
+		[System.Serializable]
+		public class PlatformOptions
+		{
+			public virtual bool IsModified()
+			{
+				return (httpHeaders.IsModified()
+				|| keyAuth.IsModified()
+				);
+			}
+
+			public HttpHeaderData httpHeaders = new HttpHeaderData();
+			public KeyAuthData keyAuth = new KeyAuthData();
+
+			// Decryption support
+			public virtual string GetKeyServerAuthToken() { return keyAuth.keyServerToken; }
+			//public virtual string GetKeyServerURL() { return null; }
+			public virtual byte[] GetOverrideDecryptionKey() { return keyAuth.overrideDecryptionKey; }
+
+			public virtual bool StartWithHighestBandwidth() { return false; }
+		}
+
+		[System.Serializable]
+		public class OptionsWindows : PlatformOptions, ISerializationCallbackReceiver
+		{
+			public Windows.VideoApi videoApi = Windows.VideoApi.MediaFoundation;
+			public bool useHardwareDecoding = true;
+			public bool useTextureMips = false;
+			public bool use10BitTextures = false;
+			public bool hintAlphaChannel = false;
+			public bool useLowLatency = false;
+			public bool useCustomMovParser = false;
+			public bool useHapNotchLC = false;
+			public bool useStereoDetection = true;
+			public bool useTextTrackSupport = true;
+			public bool useFacebookAudio360Support = true;
+			public bool useAudioDelay = false;
+			public BufferedFrameSelectionMode bufferedFrameSelection = BufferedFrameSelectionMode.None;
+			public bool pauseOnPrerollComplete = false;
+			public string forceAudioOutputDeviceName = string.Empty;
+			public List<string> preferredFilters = new List<string>();
+			public Windows.AudioOutput audioOutput = Windows.AudioOutput.System;
+			public Audio360ChannelMode audio360ChannelMode = Audio360ChannelMode.TBE_8_2;
+
+			/// WinRT only
+			public bool startWithHighestBitrate = false;
+
+			/// WinRT only
+			public bool useLowLiveLatency = false;
+
+			/// Hap & NotchLC only
+			[Range(1, 16)]
+			public int parallelFrameCount = 3;
+			/// Hap & NotchLC only
+			[Range(1, 16)]
+			public int prerollFrameCount = 4;
+
+			public override bool IsModified()
+			{
+				return (base.IsModified()
+				|| !useHardwareDecoding
+				|| useTextureMips
+				|| use10BitTextures
+				|| hintAlphaChannel
+				|| useLowLatency
+				|| useCustomMovParser
+				|| useHapNotchLC
+				|| !useStereoDetection
+				|| !useTextTrackSupport
+				|| !useFacebookAudio360Support
+				|| useAudioDelay
+				|| pauseOnPrerollComplete
+				|| bufferedFrameSelection != BufferedFrameSelectionMode.None
+				|| videoApi != Windows.VideoApi.MediaFoundation
+				|| audioOutput != Windows.AudioOutput.System
+				|| audio360ChannelMode != Audio360ChannelMode.TBE_8_2
+				|| !string.IsNullOrEmpty(forceAudioOutputDeviceName)
+				|| preferredFilters.Count != 0
+				|| startWithHighestBitrate
+				|| useLowLiveLatency
+				|| parallelFrameCount != 3
+				|| prerollFrameCount != 4
+				);
+			}
+
+			public override bool StartWithHighestBandwidth() { return startWithHighestBitrate; }
+
+			#region Upgrade from Version 1.x
+			[SerializeField, HideInInspector]
+			private bool useUnityAudio = false;
+			[SerializeField, HideInInspector]
+			private bool enableAudio360 = false;
+
+			void ISerializationCallbackReceiver.OnBeforeSerialize() { }
+
+			void ISerializationCallbackReceiver.OnAfterDeserialize()
+			{
+				if (useUnityAudio && audioOutput == Windows.AudioOutput.System)
+				{
+					audioOutput = Windows.AudioOutput.Unity;
+					useUnityAudio = false;
+				}
+				if (enableAudio360 && audioOutput == Windows.AudioOutput.System)
+				{
+					audioOutput = Windows.AudioOutput.FacebookAudio360;
+					enableAudio360 = false;
+				}
+			}
+			#endregion	// Upgrade from Version 1.x
+		}
+
+		[System.Serializable]
+		public class OptionsWindowsUWP : PlatformOptions
+		{
+			public bool useHardwareDecoding = true;
+			public bool useTextureMips = false;
+			public bool use10BitTextures = false;
+			public bool hintOutput10Bit = false;
+			public bool useLowLatency = false;
+			public WindowsUWP.VideoApi videoApi = WindowsUWP.VideoApi.WinRT;
+			public WindowsUWP.AudioOutput audioOutput = WindowsUWP.AudioOutput.System;
+			public Audio360ChannelMode audio360ChannelMode = Audio360ChannelMode.TBE_8_2;
+
+			/// WinRT only
+			public bool startWithHighestBitrate = false;
+
+			/// WinRT only
+			public bool useLowLiveLatency = false;
+
+			public override bool IsModified()
+			{
+				return (base.IsModified()
+				|| !useHardwareDecoding
+				|| useTextureMips
+				|| use10BitTextures
+				|| useLowLatency
+				|| audioOutput != WindowsUWP.AudioOutput.System
+				|| (audio360ChannelMode != Audio360ChannelMode.TBE_8_2)
+				|| videoApi != WindowsUWP.VideoApi.WinRT
+				|| startWithHighestBitrate
+				|| useLowLiveLatency
+				);
+			}
+
+			public override bool StartWithHighestBandwidth() { return startWithHighestBitrate; }
+		}
+
+		[System.Serializable]
+		public class OptionsApple: PlatformOptions
+		{
+			public enum TextureFormat: int
+			{
+				BGRA,
+				YCbCr420,
+			}
+
+			public enum AudioMode
+			{
+				SystemDirect,
+				Unity,
+				SystemDirectWithCapture,
+			};
+
+			[Flags]
+			public enum Flags: int
+			{
+				// Common
+				None = 0,
+				GenerateMipMaps = 1 << 0,
+
+				// iOS & macOS
+				AllowExternalPlayback = 1 <<  8,
+				PlayWithoutBuffering  = 1 <<  9,
+				UseSinglePlayerItem   = 1 << 10,
+
+				// iOS
+				ResumeMediaPlaybackAfterAudioSessionRouteChange = 1 << 16,
+			}
+
+			public enum Resolution
+			{
+				NoPreference,
+				_480p,
+				_720p,
+				_1080p,
+				_1440p,
+				_2160p,
+				Custom
+			}
+
+			public enum BitRateUnits
+			{
+				bps,
+				Kbps,
+				Mbps,
+			}
+
+			private readonly TextureFormat DefaultTextureFormat;
+			private readonly Flags DefaultFlags;
+			public TextureFormat textureFormat;
+			
+			private AudioMode _previousAudioMode = AudioMode.SystemDirect;
+			public AudioMode previousAudioMode
+			{
+				get { return _previousAudioMode; }
+			}
+
+			[SerializeField]
+			private AudioMode _audioMode;
+			public AudioMode audioMode
+			{
+				get { return _audioMode; }
+				set
+				{
+					if (_audioMode != value)
+					{
+						_previousAudioMode = _audioMode;
+						_audioMode = value;
+						_changed |= ChangeFlags.AudioMode;
+					}
+				}
+			}
+
+			[SerializeField]
+			private Flags _flags;
+			public Flags flags
+			{
+				get { return _flags; }
+				set
+				{
+					Flags changed = _flags ^ value;
+					if (changed != 0)
+					{
+						if ((changed & Flags.PlayWithoutBuffering) == Flags.PlayWithoutBuffering)
+						{
+							_changed |= ChangeFlags.PlayWithoutBuffering;
+						}
+						if ((changed & Flags.ResumeMediaPlaybackAfterAudioSessionRouteChange) == Flags.ResumeMediaPlaybackAfterAudioSessionRouteChange)
+						{
+							_changed |= ChangeFlags.ResumeMediaPlaybackAfterAudioSessionRouteChange;
+						}
+						_flags = value;
+					}
+				}
+			}
+
+			public float maximumPlaybackRate = 2.0f;
+
+			[Flags]
+			public enum ChangeFlags: int
+			{
+				None                                            = 0,
+				PreferredPeakBitRate                            = 1 << 1,
+				PreferredForwardBufferDuration                  = 1 << 2,
+				PlayWithoutBuffering                            = 1 << 3,
+				PreferredMaximumResolution                      = 1 << 4,
+				AudioMode                                       = 1 << 5,
+				ResumeMediaPlaybackAfterAudioSessionRouteChange = 1 << 6,
+				All = -1
+			}
+
+			private ChangeFlags _changed = ChangeFlags.None;
+
+			[SerializeField]
+			private float _preferredPeakBitRate = 0.0f;
+			public float preferredPeakBitRate
+			{
+				get { return _preferredPeakBitRate; }
+				set
+				{
+					if (_preferredPeakBitRate != value)
+					{
+						_changed |= ChangeFlags.PreferredPeakBitRate;
+						_preferredPeakBitRate = value;
+					}
+				}
+			}
+
+			[SerializeField]
+			private BitRateUnits _preferredPeakBitRateUnits = BitRateUnits.Kbps;
+			public BitRateUnits preferredPeakBitRateUnits
+			{
+				get { return _preferredPeakBitRateUnits; }
+				set
+				{
+					if (_preferredPeakBitRateUnits != value)
+					{
+						_changed |= ChangeFlags.PreferredPeakBitRate;
+						_preferredPeakBitRateUnits = value;
+					}
+				}
+			}
+
+			[SerializeField]
+			private double _preferredForwardBufferDuration = 0.0;
+			public double preferredForwardBufferDuration
+			{
+				get
+				{
+					return _preferredForwardBufferDuration;
+				}
+				set
+				{
+					if (_preferredForwardBufferDuration != value)
+					{
+						_changed |= ChangeFlags.PreferredForwardBufferDuration;
+						_preferredForwardBufferDuration = value;
+					}
+				}
+			}
+
+			[SerializeField]
+			private Resolution _preferredMaximumResolution = Resolution.NoPreference;
+			public Resolution preferredMaximumResolution
+			{
+				get
+				{
+					return _preferredMaximumResolution;
+				}
+				set
+				{
+					if (_preferredMaximumResolution != value)
+					{
+						_changed |= ChangeFlags.PreferredMaximumResolution;
+						_preferredMaximumResolution = value;
+					}
+				}
+			}
+
+#if UNITY_2017_2_OR_NEWER
+			[SerializeField]
+			private Vector2Int _customPreferredMaximumResolution = Vector2Int.zero;
+			public Vector2Int customPreferredMaximumResolution
+			{
+				get
+				{
+					return _customPreferredMaximumResolution;
+				}
+				set
+				{
+					if (_customPreferredMaximumResolution != value)
+					{
+						_changed |= ChangeFlags.PreferredMaximumResolution;
+						_customPreferredMaximumResolution = value;
+					}
+				}
+			}
+#endif
+
+			private static double BitRateInBitsPerSecond(float value, BitRateUnits units)
+			{
+				switch (units)
+				{
+					case BitRateUnits.bps:
+						return (double)value;
+					case BitRateUnits.Kbps:
+						return (double)value * 1000.0;
+					case BitRateUnits.Mbps:
+						return (double)value * 1000000.0;
+					default:
+						return 0.0;
+				}
+			}
+
+			public double GetPreferredPeakBitRateInBitsPerSecond()
+			{
+				return BitRateInBitsPerSecond(preferredPeakBitRate, preferredPeakBitRateUnits);
+			}
+
+			public OptionsApple(TextureFormat defaultTextureFormat, Flags defaultFlags)
+			{
+				DefaultTextureFormat = defaultTextureFormat;
+				DefaultFlags = defaultFlags;
+				textureFormat = defaultTextureFormat;
+				audioMode = AudioMode.SystemDirect;
+				flags = defaultFlags;
+			}
+
+			public override bool IsModified()
+			{
+				return base.IsModified()
+					|| textureFormat != DefaultTextureFormat
+					|| audioMode != AudioMode.SystemDirect
+					|| flags != DefaultFlags
+					|| preferredMaximumResolution != Resolution.NoPreference
+					|| preferredPeakBitRate != 0.0f
+					|| preferredForwardBufferDuration != 0.0;
+			}
+
+			public bool HasChanged(ChangeFlags flags = ChangeFlags.All)
+			{
+				return (_changed & flags) != ChangeFlags.None;
+			}
+
+			public void ClearChanges()
+			{
+				_changed = ChangeFlags.None;
+			}
+		}
+
+		[System.Serializable]
+		public class OptionsAndroid : PlatformOptions, ISerializationCallbackReceiver
+		{
+			public enum Resolution
+			{
+				NoPreference,
+				_480p,
+				_720p,
+				_1080p,
+				_2160p,
+				Custom
+			}
+
+			public enum BitRateUnits
+			{
+				bps,
+				Kbps,
+				Mbps,
+			}
+
+			[Flags]
+			public enum ChangeFlags : int
+			{
+				None = 0,
+				PreferredPeakBitRate = 1 << 1,
+				PreferredMaximumResolution = 1 << 2,
+				PreferredCustomResolution = 1 << 3,
+				All = -1
+			}
+
+			private ChangeFlags _changed = ChangeFlags.None;
+
+			[SerializeField]
+			private Resolution _preferredMaximumResolution = Resolution.NoPreference;
+			public Resolution preferredMaximumResolution
+			{
+				get { return _preferredMaximumResolution; }
+				set
+				{
+					if (_preferredMaximumResolution != value)
+					{
+						_changed |= ChangeFlags.PreferredMaximumResolution;
+						_preferredMaximumResolution = value;
+					}
+				}
+			}
+
+#if UNITY_2017_2_OR_NEWER
+			[SerializeField]
+			private Vector2Int _customPreferredMaximumResolution = Vector2Int.zero;
+			public Vector2Int customPreferredMaximumResolution
+			{
+				get { return _customPreferredMaximumResolution; }
+				set
+				{
+					if (_customPreferredMaximumResolution != value)
+					{
+						_changed |= ChangeFlags.PreferredCustomResolution;
+						_customPreferredMaximumResolution = value;
+					}
+				}
+			}
+#endif
+
+			[SerializeField]
+			private float _preferredPeakBitRate = 0.0f;
+			public float preferredPeakBitRate
+			{
+				get { return _preferredPeakBitRate; }
+				set
+				{
+					if (_preferredPeakBitRate != value)
+					{
+						_changed |= ChangeFlags.PreferredPeakBitRate;
+						_preferredPeakBitRate = value;
+					}
+				}
+			}
+
+			[SerializeField]
+			private BitRateUnits _preferredPeakBitRateUnits = BitRateUnits.Kbps;
+			public BitRateUnits preferredPeakBitRateUnits
+			{
+				get { return _preferredPeakBitRateUnits; }
+				set
+				{
+					if (_preferredPeakBitRateUnits != value)
+					{
+						_changed |= ChangeFlags.PreferredPeakBitRate;
+						_preferredPeakBitRateUnits = value;
+					}
+				}
+			}
+
+
+			public Android.VideoApi videoApi = Android.VideoApi.ExoPlayer;
+			public bool useFastOesPath = false;
+			public bool showPosterFrame = false;
+			public Android.AudioOutput audioOutput = Android.AudioOutput.System;
+			public Audio360ChannelMode audio360ChannelMode = Audio360ChannelMode.TBE_8_2;
+			public bool preferSoftwareDecoder = false;
+			public Android.TextureFiltering blitTextureFiltering = Android.TextureFiltering.Point;
+
+			[SerializeField, Tooltip("Byte offset into the file where the media file is located.  This is useful when hiding or packing media files within another file.")]
+			public int fileOffset = 0;
+
+			public bool startWithHighestBitrate = false;
+
+			public int minBufferMs							= Android.Default_MinBufferTimeMs;
+			public int maxBufferMs							= Android.Default_MaxBufferTimeMs;
+			public int bufferForPlaybackMs					= Android.Default_BufferForPlaybackMs;
+			public int bufferForPlaybackAfterRebufferMs		= Android.Default_BufferForPlaybackAfterRebufferMs;
+
+
+			public override bool IsModified()
+			{
+				return (base.IsModified()
+					|| (fileOffset != 0)
+					|| useFastOesPath
+					|| showPosterFrame
+					|| (videoApi != Android.VideoApi.ExoPlayer)
+					|| audioOutput != Android.AudioOutput.System
+					|| (audio360ChannelMode != Audio360ChannelMode.TBE_8_2)
+					|| preferSoftwareDecoder
+					|| startWithHighestBitrate
+					|| (minBufferMs != Android.Default_MinBufferTimeMs)
+					|| (maxBufferMs != Android.Default_MaxBufferTimeMs)
+					|| (bufferForPlaybackMs != Android.Default_BufferForPlaybackMs)
+					|| (bufferForPlaybackAfterRebufferMs != Android.Default_BufferForPlaybackAfterRebufferMs)
+					|| (preferredMaximumResolution != Resolution.NoPreference)
+					|| (preferredPeakBitRate != 0.0f)
+					|| (blitTextureFiltering != Android.TextureFiltering.Point)
+				);
+			}
+
+			private static double BitRateInBitsPerSecond(float value, BitRateUnits units)
+			{
+				switch (units)
+				{
+					case BitRateUnits.bps:
+						return (double)value;
+					case BitRateUnits.Kbps:
+						return (double)value * 1000.0;
+					case BitRateUnits.Mbps:
+						return (double)value * 1000000.0;
+					default:
+						return 0.0;
+				}
+			}
+
+			public double GetPreferredPeakBitRateInBitsPerSecond()
+			{
+				_changed &= ~ChangeFlags.PreferredPeakBitRate;
+				return BitRateInBitsPerSecond(preferredPeakBitRate, preferredPeakBitRateUnits);
+			}
+
+			public override bool StartWithHighestBandwidth()
+			{
+				return startWithHighestBitrate;
+			}
+
+			public bool HasChanged(ChangeFlags flags = ChangeFlags.All, bool bClearFlags = false)
+			{
+				bool bReturn = ((_changed & flags) != ChangeFlags.None);
+				if (bClearFlags)
+				{
+					_changed = ChangeFlags.None;
+				}
+				return bReturn;
+			}
+
+			#region Upgrade from Version 1.x
+			[SerializeField, HideInInspector]
+			private bool enableAudio360 = false;
+
+			void ISerializationCallbackReceiver.OnBeforeSerialize()	{ }
+
+			void ISerializationCallbackReceiver.OnAfterDeserialize()
+			{
+				if (enableAudio360 && audioOutput == Android.AudioOutput.System)
+				{
+					audioOutput = Android.AudioOutput.FacebookAudio360;
+					enableAudio360 = false;
+				}
+			}
+			#endregion	// Upgrade from Version 1.x
+		}
+
+		[System.Serializable]
+		public class OptionsWebGL : PlatformOptions
+		{
+			public WebGL.ExternalLibrary externalLibrary = WebGL.ExternalLibrary.None;
+			public bool useTextureMips = false;
+
+			public override bool IsModified()
+			{
+				return (base.IsModified() || externalLibrary != WebGL.ExternalLibrary.None || useTextureMips);
+			}
+
+			// Decryption support
+			public override string GetKeyServerAuthToken() { return null; }
+			public override byte[] GetOverrideDecryptionKey() { return null; }
+		}
+
+		// TODO: move these to a Setup object
+		[SerializeField] OptionsWindows _optionsWindows = new OptionsWindows();
+		[SerializeField] OptionsApple _optionsMacOSX = new OptionsApple(OptionsApple.TextureFormat.BGRA, OptionsApple.Flags.None);
+		[SerializeField] OptionsApple _optionsIOS = new OptionsApple(OptionsApple.TextureFormat.BGRA, OptionsApple.Flags.None);
+		[SerializeField] OptionsApple _optionsTVOS = new OptionsApple(OptionsApple.TextureFormat.BGRA, OptionsApple.Flags.None);
+		[SerializeField] OptionsAndroid _optionsAndroid = new OptionsAndroid();
+		[SerializeField] OptionsWindowsUWP _optionsWindowsUWP = new OptionsWindowsUWP();
+		[SerializeField] OptionsWebGL _optionsWebGL = new OptionsWebGL();
+
+		public OptionsWindows PlatformOptionsWindows { get { return _optionsWindows; } }
+		public OptionsApple PlatformOptionsMacOSX { get { return _optionsMacOSX; } }
+		public OptionsApple PlatformOptionsIOS { get { return _optionsIOS; } }
+		public OptionsApple PlatformOptionsTVOS { get { return _optionsTVOS; } }
+		public OptionsAndroid PlatformOptionsAndroid { get { return _optionsAndroid; } }
+		public OptionsWindowsUWP PlatformOptionsWindowsUWP { get { return _optionsWindowsUWP; } }
+		public OptionsWebGL PlatformOptionsWebGL { get { return _optionsWebGL; } }
+
+#endregion // PlatformOptions
+	}
+
+#region PlatformOptionsExtensions
+	public static class OptionsAppleExtensions
+	{
+		public static bool GenerateMipmaps(this MediaPlayer.OptionsApple.Flags flags)
+		{
+			return (flags & MediaPlayer.OptionsApple.Flags.GenerateMipMaps) == MediaPlayer.OptionsApple.Flags.GenerateMipMaps;
+		}
+
+		public static MediaPlayer.OptionsApple.Flags SetGenerateMipMaps(this MediaPlayer.OptionsApple.Flags flags, bool b)
+		{
+			if (flags.GenerateMipmaps() ^ b)
+			{
+				flags = b ? flags | MediaPlayer.OptionsApple.Flags.GenerateMipMaps
+				          : flags & ~MediaPlayer.OptionsApple.Flags.GenerateMipMaps;
+			}
+			return flags;
+		}
+
+		public static bool AllowExternalPlayback(this MediaPlayer.OptionsApple.Flags flags)
+		{
+			return (flags & MediaPlayer.OptionsApple.Flags.AllowExternalPlayback) == MediaPlayer.OptionsApple.Flags.AllowExternalPlayback;
+		}
+
+		public static MediaPlayer.OptionsApple.Flags SetAllowExternalPlayback(this MediaPlayer.OptionsApple.Flags flags, bool b)
+		{
+			if (flags.AllowExternalPlayback() ^ b)
+			{
+				flags = b ? flags | MediaPlayer.OptionsApple.Flags.AllowExternalPlayback
+				          : flags & ~MediaPlayer.OptionsApple.Flags.AllowExternalPlayback;
+			}
+			return flags;
+		}
+
+		public static bool PlayWithoutBuffering(this MediaPlayer.OptionsApple.Flags flags)
+		{
+			return (flags & MediaPlayer.OptionsApple.Flags.PlayWithoutBuffering) == MediaPlayer.OptionsApple.Flags.PlayWithoutBuffering;
+		}
+
+		public static MediaPlayer.OptionsApple.Flags SetPlayWithoutBuffering(this MediaPlayer.OptionsApple.Flags flags, bool b)
+		{
+			if (flags.PlayWithoutBuffering() ^ b)
+			{
+				flags = b ? flags | MediaPlayer.OptionsApple.Flags.PlayWithoutBuffering
+						  : flags & ~MediaPlayer.OptionsApple.Flags.PlayWithoutBuffering;
+			}
+			return flags;
+		}
+
+		public static bool UseSinglePlayerItem(this MediaPlayer.OptionsApple.Flags flags)
+		{
+			return (flags & MediaPlayer.OptionsApple.Flags.UseSinglePlayerItem) == MediaPlayer.OptionsApple.Flags.UseSinglePlayerItem;
+		}
+
+		public static MediaPlayer.OptionsApple.Flags SetUseSinglePlayerItem(this MediaPlayer.OptionsApple.Flags flags, bool b)
+		{
+			if (flags.UseSinglePlayerItem() ^ b)
+			{
+				flags = b ? flags | MediaPlayer.OptionsApple.Flags.UseSinglePlayerItem
+						  : flags & ~MediaPlayer.OptionsApple.Flags.UseSinglePlayerItem;
+			}
+			return flags;
+		}
+
+		public static bool ResumePlaybackAfterAudioSessionRouteChange(this MediaPlayer.OptionsApple.Flags flags)
+		{
+			return (flags & MediaPlayer.OptionsApple.Flags.ResumeMediaPlaybackAfterAudioSessionRouteChange) == MediaPlayer.OptionsApple.Flags.ResumeMediaPlaybackAfterAudioSessionRouteChange;
+		}
+
+		public static MediaPlayer.OptionsApple.Flags SetResumePlaybackAfterAudioSessionRouteChange(this MediaPlayer.OptionsApple.Flags flags, bool b)
+		{
+			if (flags.ResumePlaybackAfterAudioSessionRouteChange() ^ b)
+			{
+				flags = b ? flags | MediaPlayer.OptionsApple.Flags.ResumeMediaPlaybackAfterAudioSessionRouteChange
+				          : flags & ~MediaPlayer.OptionsApple.Flags.ResumeMediaPlaybackAfterAudioSessionRouteChange;
+			}
+			return flags;
+		}
+	}
+#endregion // PlatformOptionsExtensions
+}

+ 12 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_PlatformOptions.cs.meta

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

+ 150 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_Subtitles.cs

@@ -0,0 +1,150 @@
+using UnityEngine;
+using System.Collections;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	public partial class MediaPlayer : MonoBehaviour
+	{
+		public bool EnableSubtitles(MediaPath mediaPath)
+		{
+			bool result = false;
+			if (_subtitlesInterface != null)
+			{
+				if (mediaPath != null && !string.IsNullOrEmpty(mediaPath.Path))
+				{
+					string fullPath = mediaPath.GetResolvedFullPath();
+
+					bool checkForFileExist = true;
+					if (fullPath.Contains("://"))
+					{
+						checkForFileExist = false;
+					}
+#if (!UNITY_EDITOR && UNITY_ANDROID)
+					checkForFileExist = false;
+#endif
+
+					if (checkForFileExist && !System.IO.File.Exists(fullPath))
+					{
+						Debug.LogError("[AVProVideo] Subtitle file not found: " + fullPath, this);
+					}
+					else
+					{
+						Helper.LogInfo("Opening subtitles " + fullPath, this);
+
+						_previousSubtitleIndex = -1;
+
+						try
+						{
+							if (fullPath.Contains("://"))
+							{
+								// Use coroutine and WWW class for loading
+								if (_loadSubtitlesRoutine != null)
+								{
+									StopCoroutine(_loadSubtitlesRoutine);
+									_loadSubtitlesRoutine = null;
+								}
+								_loadSubtitlesRoutine = StartCoroutine(LoadSubtitlesCoroutine(fullPath, mediaPath));
+							}
+							else
+							{
+								// Load directly from file
+								string subtitleData = System.IO.File.ReadAllText(fullPath);
+								if (_subtitlesInterface.LoadSubtitlesSRT(subtitleData))
+								{
+									_subtitlePath = mediaPath;
+									_sideloadSubtitles = false;
+									result = true;
+								}
+								else
+								{
+									Debug.LogError("[AVProVideo] Failed to load subtitles" + fullPath, this);
+								}
+							}
+
+						}
+						catch (System.Exception e)
+						{
+							Debug.LogError("[AVProVideo] Failed to load subtitles " + fullPath, this);
+							Debug.LogException(e, this);
+						}
+					}
+				}
+				else
+				{
+					Debug.LogError("[AVProVideo] No subtitle file path specified", this);
+				}
+			}
+			else
+			{
+				_queueSubtitlePath = mediaPath;
+			}
+
+			return result;
+		}
+
+		private IEnumerator LoadSubtitlesCoroutine(string url, MediaPath mediaPath)
+		{
+			UnityEngine.Networking.UnityWebRequest www = UnityEngine.Networking.UnityWebRequest.Get(url);
+			#if UNITY_2017_2_OR_NEWER
+			yield return www.SendWebRequest();
+			#else
+			yield return www.Send();
+			#endif
+
+			string subtitleData = string.Empty;
+
+			#if UNITY_2020_1_OR_NEWER
+			if (www.result == UnityEngine.Networking.UnityWebRequest.Result.Success)
+			#elif UNITY_2017_1_OR_NEWER
+			if (!www.isNetworkError)
+			#else
+			if (!www.isError)
+			#endif
+			{
+				subtitleData = ((UnityEngine.Networking.DownloadHandler)www.downloadHandler).text;
+			}
+			else
+			{
+				Debug.LogError("[AVProVideo] Error loading subtitles '" + www.error + "' from " + url);
+			}
+
+			if (_subtitlesInterface.LoadSubtitlesSRT(subtitleData))
+			{
+				_subtitlePath = mediaPath;
+				_sideloadSubtitles = false;
+			}
+			else
+			{
+				Debug.LogError("[AVProVideo] Failed to load subtitles" + url, this);
+			}
+
+			_loadSubtitlesRoutine = null;
+
+			www.Dispose();
+		}
+
+		public void DisableSubtitles()
+		{
+			if (_loadSubtitlesRoutine != null)
+			{
+				StopCoroutine(_loadSubtitlesRoutine);
+				_loadSubtitlesRoutine = null;
+			}
+
+			if (_subtitlesInterface != null)
+			{
+				_previousSubtitleIndex = -1;
+				_sideloadSubtitles = false;
+				_subtitlesInterface.LoadSubtitlesSRT(string.Empty);
+			}
+			else
+			{
+				_queueSubtitlePath = null;
+			}
+		}
+	}
+}

+ 12 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_Subtitles.cs.meta

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

+ 93 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_TimeScale.cs

@@ -0,0 +1,93 @@
+using UnityEngine;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	public partial class MediaPlayer : MonoBehaviour
+	{
+
+#region Support for Time Scale
+#if AVPROVIDEO_BETA_SUPPORT_TIMESCALE
+		// Adjust this value to get faster performance but may drop frames.
+		// Wait longer to ensure there is enough time for frames to process
+		private const float TimeScaleTimeoutMs = 20f;
+		private bool _timeScaleIsControlling;
+		private double _timeScaleVideoTime;
+
+		private void UpdateTimeScale()
+		{
+			if (Time.timeScale != 1f || Time.captureFramerate != 0)
+			{
+				if (_controlInterface.IsPlaying())
+				{
+					_controlInterface.Pause();
+					_timeScaleIsControlling = true;
+					_timeScaleVideoTime = _controlInterface.GetCurrentTime();
+				}
+
+				if (_timeScaleIsControlling)
+				{
+					// Progress time
+					_timeScaleVideoTime += Time.deltaTime;
+
+					// Handle looping
+					if (_controlInterface.IsLooping() && _timeScaleVideoTime >= Info.GetDuration())
+					{
+						// TODO: really we should seek to (_timeScaleVideoTime % Info.GetDuration())
+						_timeScaleVideoTime = 0.0;
+					}
+
+					int preSeekFrameCount = TextureProducer.GetTextureFrameCount();
+
+					// Seek to the new time
+					{
+						double preSeekTime = Control.GetCurrentTime();
+
+						// Seek
+						_controlInterface.Seek(_timeScaleVideoTime);
+
+						// Early out, if after the seek the time hasn't changed, the seek was probably too small to go to the next frame.
+						// TODO: This behaviour may be different on other platforms (not Windows) and needs more testing.
+						if (Mathf.Approximately((float)preSeekTime, (float)_controlInterface.GetCurrentTime()))
+						{
+							return;
+						}
+					}
+
+					// Wait for the new frame to arrive
+					if (!_controlInterface.WaitForNextFrame(GetDummyCamera(), preSeekFrameCount))
+					{
+						// If WaitForNextFrame fails (e.g. in android single threaded), we run the below code to asynchronously wait for the frame
+						System.DateTime startTime = System.DateTime.Now;
+						int lastFrameCount = TextureProducer.GetTextureFrameCount();
+
+						while (_controlInterface != null && (System.DateTime.Now - startTime).TotalMilliseconds < (double)TimeScaleTimeoutMs)
+						{
+							_playerInterface.Update();
+							_playerInterface.Render();
+							GetDummyCamera().Render();
+							if (lastFrameCount != TextureProducer.GetTextureFrameCount())
+							{
+								break;
+							}
+						}
+					}
+				}
+			}
+			else
+			{
+				// Restore playback when timeScale becomes 1
+				if (_timeScaleIsControlling)
+				{
+					_controlInterface.Play();
+					_timeScaleIsControlling = false;
+				}
+			}
+		}
+#endif
+#endregion // Support for Time Scale
+	}
+}

+ 12 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_TimeScale.cs.meta

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

+ 83 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_Upgrade.cs

@@ -0,0 +1,83 @@
+using UnityEngine;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	public partial class MediaPlayer : MonoBehaviour, ISerializationCallbackReceiver
+	{
+		#region Upgrade from Version 1.x
+		[SerializeField, HideInInspector]
+		private string m_VideoPath;
+		[SerializeField, HideInInspector]
+		private FileLocation m_VideoLocation = FileLocation.RelativeToStreamingAssetsFolder;
+
+		private enum FileLocation
+		{
+			AbsolutePathOrURL,
+			RelativeToProjectFolder,
+			RelativeToStreamingAssetsFolder,
+			RelativeToDataFolder,
+			RelativeToPersistentDataFolder,
+		}
+
+		/*
+		[SerializeField, HideInInspector]
+		private StereoPacking m_StereoPacking;
+		[SerializeField, HideInInspector]
+		private AlphaPacking m_AlphaPacking;
+		*/
+
+		void ISerializationCallbackReceiver.OnBeforeSerialize()
+		{
+			/*
+			m_StereoPacking = _fallbackMediaHints.stereoPacking;
+			m_AlphaPacking = _fallbackMediaHints.alphaPacking;
+			*/
+		}
+
+		void ISerializationCallbackReceiver.OnAfterDeserialize()
+		{
+			if (!string.IsNullOrEmpty(m_VideoPath))
+			{
+				MediaPathType mediaPathType = MediaPathType.AbsolutePathOrURL;
+				switch (m_VideoLocation)
+				{
+					default:
+					case FileLocation.AbsolutePathOrURL:
+						mediaPathType = MediaPathType.AbsolutePathOrURL;
+						break;
+					case FileLocation.RelativeToProjectFolder:
+						mediaPathType = MediaPathType.RelativeToProjectFolder;
+						break;
+					case FileLocation.RelativeToStreamingAssetsFolder:
+						mediaPathType = MediaPathType.RelativeToStreamingAssetsFolder;
+						break;
+					case FileLocation.RelativeToDataFolder:
+						mediaPathType = MediaPathType.RelativeToDataFolder;
+						break;
+					case FileLocation.RelativeToPersistentDataFolder:
+						mediaPathType = MediaPathType.RelativeToPersistentDataFolder;
+						break;
+				}
+				_mediaPath = new MediaPath(m_VideoPath, mediaPathType);
+				_mediaSource = MediaSource.Path;
+				m_VideoPath = null;
+			}
+
+			/*
+			if (m_StereoPacking != _fallbackMediaHints.stereoPacking)
+			{
+				_fallbackMediaHints.stereoPacking = m_StereoPacking;
+			}
+			if (m_AlphaPacking != _fallbackMediaHints.alphaPacking)
+			{
+				_fallbackMediaHints.alphaPacking = m_AlphaPacking;
+			}
+			*/
+		}
+		#endregion	// Upgrade from Version 1.x
+	}
+}

+ 12 - 0
Assets/AVProVideo/Runtime/Scripts/Components/MediaPlayer_Upgrade.cs.meta

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

+ 1012 - 0
Assets/AVProVideo/Runtime/Scripts/Components/PlaylistMediaPlayer.cs

@@ -0,0 +1,1012 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	[System.Serializable]
+	public class MediaPlaylist
+	{
+		[System.Serializable]
+		public class MediaItem
+		{
+			public enum SourceType
+			{
+				AVProVideoPlayer,
+				//Texture2D,
+			}
+
+			[SerializeField] public string name = string.Empty;
+			[SerializeField] public SourceType sourceType = SourceType.AVProVideoPlayer;
+			[SerializeField] public MediaPath mediaPath = new MediaPath();
+			[SerializeField] public Texture2D texture = null;
+			[SerializeField] public float textureDuration;
+			[SerializeField] public bool loop = false;
+			[SerializeField] public PlaylistMediaPlayer.StartMode startMode = PlaylistMediaPlayer.StartMode.Immediate;
+			[SerializeField] public PlaylistMediaPlayer.ProgressMode progressMode = PlaylistMediaPlayer.ProgressMode.OnFinish;
+			[SerializeField] public float progressTimeSeconds = 0.5f;
+			[SerializeField] public bool isOverrideTransition = false;
+			[SerializeField] public PlaylistMediaPlayer.Transition overrideTransition = PlaylistMediaPlayer.Transition.None;
+			[SerializeField] public float overrideTransitionDuration = 1f;
+			[SerializeField] public PlaylistMediaPlayer.Easing.Preset overrideTransitionEasing = PlaylistMediaPlayer.Easing.Preset.Linear;
+		}
+
+		[SerializeField] List<MediaItem> _items = new List<MediaItem>(8);
+		public List<MediaItem> Items { get { return _items; } }
+
+		public bool HasItemAt(int index)
+		{
+			return (index >= 0 && index < _items.Count);
+		}
+	}
+
+	/// <summary>
+	/// This is a BETA component
+	/// </summary>
+	[AddComponentMenu("AVPro Video/Playlist Media Player (BETA)", -80)]
+	[HelpURL("https://www.renderheads.com/products/avpro-video/")]
+	public class PlaylistMediaPlayer : MediaPlayer, ITextureProducer
+	{
+		public enum Transition
+		{
+			None,
+			Fade,
+			Black,
+			White,
+			Transparent,
+			Horiz,
+			Vert,
+			Diag,
+			MirrorH,
+			MirrorV,
+			MirrorD,
+			ScrollV,
+			ScrollH,
+			Circle,
+			Diamond,
+			Blinds,
+			Arrows,
+			SlideH,
+			SlideV,
+			Zoom,
+			RectV,
+			Random,
+		}
+
+		public enum PlaylistLoopMode
+		{
+			None,
+			Loop,
+		}
+
+		public enum StartMode
+		{
+			Immediate,
+			//AfterSeconds,
+			Manual,
+		}
+
+		public enum ProgressMode
+		{
+			OnFinish,
+			BeforeFinish,
+			//AfterTime,
+			Manual,
+		}
+		[SerializeField] Shader _transitionShader = null;
+
+		[SerializeField] MediaPlayer _playerA = null;
+		[SerializeField] MediaPlayer _playerB = null;
+		[SerializeField] bool _playlistAutoProgress = true;
+
+		[Tooltip("Close the video on the other MediaPlayer when it is not visible any more.  This is useful for freeing up memory and GPU decoding resources.")]
+		[SerializeField] bool _autoCloseVideo = true;
+
+		[SerializeField] PlaylistLoopMode _playlistLoopMode = PlaylistLoopMode.None;
+		[SerializeField] MediaPlaylist _playlist = new MediaPlaylist();
+
+		[Tooltip("Pause the previously playing video. This is useful for systems that will struggle to play 2 videos at once")]
+		[SerializeField] bool _pausePreviousOnTransition = true;
+
+		[SerializeField] Transition _defaultTransition = Transition.None;
+		[SerializeField] float _defaultTransitionDuration = 1f;
+		[SerializeField] Easing.Preset _defaultTransitionEasing = Easing.Preset.Linear;
+
+		private static readonly LazyShaderProperty PropFromTex = new LazyShaderProperty("_FromTex");
+		private static readonly LazyShaderProperty PropFade = new LazyShaderProperty("_Fade");
+
+		private bool _isPaused = false;
+		private int _playlistIndex = 0;
+		private MediaPlayer _nextPlayer;
+		private Material _material;
+		private Transition _currentTransition = Transition.None;
+		private string _currentTransitionName = "LERP_NONE";
+		private float _currentTransitionDuration = 1f;
+		private Easing.Preset _currentTransitionEasing = Easing.Preset.Linear;
+		private float _textureTimer;
+		private float _transitionTimer;
+		private System.Func<float, float> _easeFunc;
+		private RenderTexture _rt;
+		private MediaPlaylist.MediaItem _currentItem;
+		private MediaPlaylist.MediaItem _nextItem;
+
+		public MediaPlayer CurrentPlayer
+		{
+			get
+			{
+				if (NextPlayer == _playerA)
+				{
+					return _playerB;
+				}
+				return _playerA;
+			}
+		}
+
+		public MediaPlayer NextPlayer
+		{
+			get
+			{
+				return _nextPlayer;
+			}
+		}
+
+		public MediaPlaylist Playlist { get { return _playlist; } }
+
+		public int PlaylistIndex { get { return _playlistIndex; } }
+
+		public MediaPlaylist.MediaItem PlaylistItem { get { if (_playlist.HasItemAt(_playlistIndex)) return _playlist.Items[_playlistIndex]; return null; } }
+
+		/// <summary>
+		/// The default transition to use if the transition is not overridden in the MediaItem
+		/// </summary>
+		public Transition DefaultTransition { get { return _defaultTransition; } set { _defaultTransition = value; } }
+		
+		/// <summary>
+		/// The default duration the transition will take (in seconds) if the transition is not overridden in the MediaItem
+		/// </summary>
+		public float DefaultTransitionDuration { get { return _defaultTransitionDuration; } set { _defaultTransitionDuration = value; } }
+
+		/// <summary>
+		/// The default easing the transition will use if the transition is not overridden in the MediaItem
+		/// </summary>
+		public Easing.Preset DefaultTransitionEasing { get { return _defaultTransitionEasing; } set { _defaultTransitionEasing = value; } }
+
+		/// <summary>
+		/// Closes videos that aren't playing.  This will save memory but adds extra overhead
+		/// </summary>
+		public bool AutoCloseVideo { get { return _autoCloseVideo; } set { _autoCloseVideo = value; } }
+
+		/// <summary>
+		/// None: Do not loop the playlist when the end is reached.<br/>Loop: Rewind the playlist and play again when the each is reached
+		/// </summary>
+		public PlaylistLoopMode LoopMode { get { return _playlistLoopMode; } set { _playlistLoopMode = value; } }
+
+		/// <summary>
+		/// Enable the playlist to progress to the next item automatically, or wait for manual trigger via scripting
+		/// </summary>
+		public bool AutoProgress { get { return _playlistAutoProgress; } set { _playlistAutoProgress = value; } }
+
+		/// <summary>
+		/// Returns the IMediaInfo interface for the MediaPlayer that is playing the current active item in the playlist (returned by CurrentPlayer property).  This will change during each transition.
+		/// </summary>
+		public override IMediaInfo Info
+		{
+			get { if (CurrentPlayer != null) return CurrentPlayer.Info; return null; }
+		}
+
+		/// <summary>
+		/// Returns the IMediaControl interface for the MediaPlayer that is playing the current active item in the playlist (returned by CurrentPlayer property).  This will change during each transition.
+		/// </summary>
+		public override IMediaControl Control
+		{
+			get { if (CurrentPlayer != null) return CurrentPlayer.Control; return null; }
+		}
+
+		public override ITextureProducer TextureProducer
+		{
+			get
+			{
+				if (CurrentPlayer != null)
+				{
+					if (IsTransitioning())
+					{
+						return this;
+					}
+					/*if (_currentItem != null && _currentItem.sourceType == MediaPlaylist.MediaItem.SourceType.Texture2D && _currentItem.texture != null)
+					{
+						return this;
+					}*/
+					return CurrentPlayer.TextureProducer;
+				}
+				return null;
+			}
+		}
+
+		[SerializeField, Range(0.0f, 1.0f)] float _playlistAudioVolume = 1.0f;
+		[SerializeField] bool _playlistAudioMuted = false;
+
+		public override float AudioVolume
+		{
+			get { return _playlistAudioVolume; }
+			set { _playlistAudioVolume = Mathf.Clamp01(value); if (!IsTransitioning() && CurrentPlayer != null) CurrentPlayer.AudioVolume = _playlistAudioVolume; }
+		}
+
+		public override bool AudioMuted
+		{
+			get { return _playlistAudioMuted; } 
+			set { _playlistAudioMuted = value; if (!IsTransitioning() && CurrentPlayer != null) CurrentPlayer.AudioMuted = _playlistAudioMuted; } 
+		}
+
+		public override void Play()
+		{
+			_isPaused = false;
+			if (Control != null)
+			{
+				Control.Play();
+			}
+			if (IsTransitioning())
+			{
+				if (!_pausePreviousOnTransition && NextPlayer.Control != null)
+				{
+					NextPlayer.Control.Play();
+				}
+			}
+		}
+
+		public override void Pause()
+		{
+			_isPaused = true;
+			if (Control != null)
+			{
+				Control.Pause();
+			}
+			if (IsTransitioning())
+			{
+				if (NextPlayer.Control != null)
+				{
+					NextPlayer.Control.Pause();
+				}
+			}
+		}
+
+		public bool IsPaused()
+		{
+			return _isPaused;
+		}
+
+		private void SwapPlayers()
+		{
+			// Pause the previously playing video
+			// This is useful for systems that will struggle to play 2 videos at once
+			if (_pausePreviousOnTransition)
+			{
+				CurrentPlayer.Pause();
+			}
+
+			// Tell listeners that the playlist item has changed
+			Events.Invoke(this, MediaPlayerEvent.EventType.PlaylistItemChanged, ErrorCode.None);
+
+			// Start the transition
+			if (_currentTransition != Transition.None)
+			{
+				// Create a new transition texture if required
+				Texture currentTexture = GetCurrentTexture();
+				Texture nextTexture = GetNextTexture();
+				if (currentTexture != null && nextTexture != null)
+				{
+					int maxWidth = Mathf.Max(nextTexture.width, currentTexture.width);
+					int maxHeight = Mathf.Max(nextTexture.height, currentTexture.height);
+					if (_rt != null)
+					{
+						if (_rt.width != maxWidth || _rt.height != maxHeight)
+						{
+							RenderTexture.ReleaseTemporary(_rt);
+							_rt = null;
+						}
+					}
+
+					if (_rt == null)
+					{
+						_rt = RenderTexture.GetTemporary(maxWidth, maxHeight, 0, RenderTextureFormat.Default, RenderTextureReadWrite.Default, 1);
+					}
+					Graphics.Blit(currentTexture, _rt);
+
+					_material.SetTexture(PropFromTex.Id, currentTexture);
+
+					_easeFunc = Easing.GetFunction(_currentTransitionEasing);
+					_transitionTimer = 0f;
+				}
+				else
+				{
+					// Immediately complete the transition
+					_transitionTimer = _currentTransitionDuration;
+
+					// Immediately update the audio volume
+					NextPlayer.AudioVolume = this.AudioVolume;
+					CurrentPlayer.AudioVolume = 0f;
+
+					if (_autoCloseVideo)
+					{
+						CurrentPlayer.MediaPath.Path = string.Empty;
+						CurrentPlayer.CloseMedia();
+					}
+				}
+			}
+
+			// Swap the videos
+			if (NextPlayer == _playerA)
+			{
+				_nextPlayer = _playerB;
+			}
+			else
+			{
+				_nextPlayer = _playerA;
+			}
+
+			// Swap the items
+			_currentItem = _nextItem;
+			_nextItem = null;
+		}
+
+		private Texture GetCurrentTexture()
+		{
+			/*if (_currentItem != null && _currentItem.sourceType == MediaPlaylist.MediaItem.SourceType.Texture2D && _currentItem.texture != null)
+			{
+				return _currentItem.texture;
+			}
+			else*/ if (CurrentPlayer != null && CurrentPlayer.TextureProducer != null)
+			{
+				return CurrentPlayer.TextureProducer.GetTexture();
+			}
+			return null;
+		}
+
+		private Texture GetNextTexture()
+		{
+			/*if (_nextItem != null && _nextItem.sourceType == MediaPlaylist.MediaItem.SourceType.Texture2D && _nextItem.texture != null)
+			{
+				return _nextItem.texture;
+			}
+			else*/ if (_nextPlayer != null && _nextPlayer.TextureProducer != null)
+			{
+				return _nextPlayer.TextureProducer.GetTexture();
+			}
+			return null;
+		}
+
+		private void Awake()
+		{
+			#if UNITY_IOS && !UNITY_EDITOR_OSX
+				Application.targetFrameRate = 60;
+			#endif
+			_nextPlayer = _playerA;
+			if (_transitionShader == null)
+			{
+				_transitionShader = Shader.Find("AVProVideo/Internal/Transition");
+				if (_transitionShader == null)
+				{
+					Debug.LogError("[AVProVideo] Missing transition shader");
+				}
+			}
+			_material = new Material(_transitionShader);
+			_easeFunc = Easing.GetFunction(_defaultTransitionEasing);
+		}
+
+		protected override void OnDestroy()
+		{
+			if (_rt != null)
+			{
+				RenderTexture.ReleaseTemporary(_rt);
+				_rt = null;
+			}
+			if (_material != null)
+			{
+				if (Application.isPlaying)
+				{
+					Material.Destroy(_material);
+				}
+				else
+				{
+					Material.DestroyImmediate(_material);
+				}
+				_material = null;
+			}
+			base.OnDestroy();
+		}
+
+		private void Start()
+		{
+			if (Application.isPlaying)
+			{
+				if (CurrentPlayer)
+				{
+					CurrentPlayer.Events.AddListener(OnVideoEvent);
+
+					if (NextPlayer)
+					{
+						NextPlayer.Events.AddListener(OnVideoEvent);
+					}
+				}
+
+				JumpToItem(0);
+			}
+		}
+
+		public void OnVideoEvent(MediaPlayer mp, MediaPlayerEvent.EventType et, ErrorCode errorCode)
+		{
+			if (mp == CurrentPlayer)
+			{
+				Events.Invoke(mp, et, errorCode);
+			}
+
+			switch (et)
+			{
+				case MediaPlayerEvent.EventType.FirstFrameReady:
+					if (mp == NextPlayer)
+					{
+						SwapPlayers();
+						Events.Invoke(mp, et, errorCode);
+					}
+					break;
+				case MediaPlayerEvent.EventType.FinishedPlaying:
+					if (_playlistAutoProgress && mp == CurrentPlayer && _currentItem.progressMode == ProgressMode.OnFinish)
+					{
+						NextItem();
+					}
+					break;
+			}
+		}
+
+		public bool PrevItem()
+		{
+			return JumpToItem(_playlistIndex - 1);
+		}
+
+		public bool NextItem()
+		{
+			bool result = JumpToItem(_playlistIndex + 1);
+			if (!result)
+			{
+				Events.Invoke(this, MediaPlayerEvent.EventType.PlaylistFinished, ErrorCode.None);
+			}
+			return result;
+		}
+
+		public bool CanJumpToItem(int index)
+		{
+			if (_playlistLoopMode == PlaylistLoopMode.Loop)
+			{
+				if (_playlist.Items.Count > 0)
+				{
+					index %= _playlist.Items.Count;
+					if (index < 0)
+					{
+						index += _playlist.Items.Count;
+					}
+				}
+			}
+			return _playlist.HasItemAt(index);
+		}
+
+		public bool JumpToItem(int index)
+		{
+			if (_playlistLoopMode == PlaylistLoopMode.Loop)
+			{
+				if (_playlist.Items.Count > 0)
+				{
+					index %= _playlist.Items.Count;
+					if (index < 0)
+					{
+						index += _playlist.Items.Count;
+					}
+				}
+			}
+			if (_playlist.HasItemAt(index))
+			{
+				_playlistIndex = index;
+				_nextItem = _playlist.Items[_playlistIndex];
+				OpenVideoFile(_nextItem);
+				return true;
+			}
+			return false;
+		}
+
+		public void OpenVideoFile(MediaPlaylist.MediaItem mediaItem)
+ 		{
+			bool isMediaAlreadyLoaded = false;
+			if (NextPlayer.MediaPath == mediaItem.mediaPath)
+			{
+				isMediaAlreadyLoaded = true;
+			}
+
+			if (!mediaItem.isOverrideTransition)
+			{
+				SetTransition(_defaultTransition, _defaultTransitionDuration, _defaultTransitionEasing);
+			}
+			else
+			{
+				SetTransition(mediaItem.overrideTransition, mediaItem.overrideTransitionDuration, mediaItem.overrideTransitionEasing);
+			}
+
+			this.Loop = NextPlayer.Loop = mediaItem.loop;
+			NextPlayer.MediaPath = new MediaPath(mediaItem.mediaPath);
+			this.MediaPath = new MediaPath(mediaItem.mediaPath);
+			NextPlayer.AudioMuted = _playlistAudioMuted;
+			NextPlayer.AudioVolume = _playlistAudioVolume;
+			if (_transitionTimer < _currentTransitionDuration && _currentTransition != Transition.None)
+			{
+				NextPlayer.AudioVolume = 0f;
+			}
+
+			if (isMediaAlreadyLoaded)
+			{
+				NextPlayer.Rewind(false);
+				if (_nextItem.startMode == StartMode.Immediate)
+				{
+					NextPlayer.Play();
+				}
+				// TODO: We probably want to wait until the new frame arrives before swapping after a Rewind()
+				SwapPlayers();
+			}
+			else
+			{
+				if (string.IsNullOrEmpty(NextPlayer.MediaPath.Path))
+				{
+					NextPlayer.CloseMedia();
+				}
+				else
+				{
+					NextPlayer.OpenMedia(NextPlayer.MediaPath.PathType, NextPlayer.MediaPath.Path, _nextItem.startMode == StartMode.Immediate);
+				}
+			}
+		}
+
+		private bool IsTransitioning()
+		{
+			if (_rt != null && _transitionTimer < _currentTransitionDuration && _currentTransition != Transition.None)
+			{
+				return true;
+			}
+			return false;
+		}
+
+		private void SetTransition(Transition transition, float duration, Easing.Preset easing)
+		{
+			if (transition == Transition.Random)
+			{
+				transition = (Transition)Random.Range(0, (int)Transition.Random);
+			}
+
+			if (transition != _currentTransition)
+			{
+				// Disable the previous transition
+				if (!string.IsNullOrEmpty(_currentTransitionName))
+				{
+					_material.DisableKeyword(_currentTransitionName);
+				}
+
+				// Enable the next transition
+				_currentTransition = transition;
+				_currentTransitionName = GetTransitionName(transition);
+				_material.EnableKeyword(_currentTransitionName);
+			}
+
+			_currentTransitionDuration = duration;
+			_currentTransitionEasing = easing;
+		}
+
+		protected override void Update()
+		{
+			if (!Application.isPlaying) return;
+
+			if (!IsPaused())
+			{
+				if (IsTransitioning())
+				{
+					_transitionTimer += Time.deltaTime;
+					float t = _easeFunc(Mathf.Clamp01(_transitionTimer / _currentTransitionDuration));
+
+					// Fade the audio volume
+					NextPlayer.AudioVolume = (1f - t) * this.AudioVolume;
+					CurrentPlayer.AudioVolume = t * this.AudioVolume;
+
+					// TODO: support going from mono to stereo
+					// TODO: support videos of different aspect ratios by rendering with scaling to fit
+					// This can be done by blitting twice, once for each eye
+					// If the stereo mode is different for playera/b then both should be set to stereo during the transition
+					// if (CurrentPlayer.m_StereoPacking == StereoPacking.TopBottom)....
+					_material.SetFloat(PropFade.Id, t);
+					_rt.DiscardContents();
+					Graphics.Blit(GetCurrentTexture(), _rt, _material);
+
+					// After the transition is now complete, close/pause the previous video if required
+					bool isTransitioning = IsTransitioning();
+					if (!isTransitioning)
+					{
+						if (_autoCloseVideo)
+						{
+							if (NextPlayer != null)
+							{
+								NextPlayer.MediaPath.Path = string.Empty;
+								NextPlayer.CloseMedia();
+							}
+						}
+						else if (!_pausePreviousOnTransition)
+						{
+							if (NextPlayer != null && NextPlayer.Control.IsPlaying())
+							{
+								NextPlayer.Pause();
+							}
+						}
+					}
+				}
+				else
+				{
+					if (_playlistAutoProgress && _nextItem == null && _currentItem != null && _currentItem.progressMode == ProgressMode.BeforeFinish && Control != null && Control.HasMetaData() && Control.GetCurrentTime() >= (Info.GetDuration() - (_currentItem.progressTimeSeconds)))
+					{
+						this.NextItem();
+					}
+					else if (_playlistAutoProgress && _currentItem == null)
+					{
+						JumpToItem(_playlistIndex);
+					}
+				}
+			}
+
+			base.Update();
+		}
+
+#region Implementing ITextureProducer
+		public Texture GetTexture(int index = 0)
+		{
+			// TODO: support iOS YCbCr by supporting multiple textures
+			/*if (!IsTransitioning())
+			{
+				if (_currentItem != null && _currentItem.sourceType == MediaPlaylist.MediaItem.SourceType.Texture2D && _currentItem.texture != null)
+				{
+					return _currentItem.texture;
+				}
+			}*/
+			return _rt;
+		}
+
+		public int GetTextureCount()
+		{
+			return CurrentPlayer.TextureProducer.GetTextureCount();
+		}
+
+		public int GetTextureFrameCount()
+		{
+			return CurrentPlayer.TextureProducer.GetTextureFrameCount();
+		}
+
+		public bool SupportsTextureFrameCount()
+		{
+			return CurrentPlayer.TextureProducer.SupportsTextureFrameCount();
+		}
+
+		public long GetTextureTimeStamp()
+		{
+			return CurrentPlayer.TextureProducer.GetTextureTimeStamp();
+		}
+
+		public float GetTexturePixelAspectRatio()
+		{
+			return CurrentPlayer.TextureProducer.GetTexturePixelAspectRatio();
+		}
+
+		public bool RequiresVerticalFlip()
+		{
+			return CurrentPlayer.TextureProducer.RequiresVerticalFlip();
+		}
+
+		public Matrix4x4 GetYpCbCrTransform()
+		{
+			return CurrentPlayer.TextureProducer.GetYpCbCrTransform();
+		}
+
+		public StereoPacking GetTextureStereoPacking()
+		{
+			return CurrentPlayer.TextureProducer.GetTextureStereoPacking();
+		}
+
+		public TransparencyMode GetTextureTransparency()
+		{
+			return CurrentPlayer.TextureProducer.GetTextureTransparency();
+		}
+
+		public AlphaPacking GetTextureAlphaPacking()
+		{
+			return CurrentPlayer.TextureProducer.GetTextureAlphaPacking();
+		}
+#endregion Implementing ITextureProducer
+
+		private static string GetTransitionName(Transition transition)
+		{
+			switch (transition)
+			{
+				case Transition.None:		return "LERP_NONE";
+				case Transition.Fade: 		return "LERP_FADE";
+				case Transition.Black:		return "LERP_BLACK";
+				case Transition.White:		return "LERP_WHITE";
+				case Transition.Transparent:return "LERP_TRANSP";
+				case Transition.Horiz:		return "LERP_HORIZ";
+				case Transition.Vert:		return "LERP_VERT";
+				case Transition.Diag:		return "LERP_DIAG";
+				case Transition.MirrorH:	return "LERP_HORIZ_MIRROR";
+				case Transition.MirrorV:	return "LERP_VERT_MIRROR";
+				case Transition.MirrorD:	return "LERP_DIAG_MIRROR";
+				case Transition.ScrollV:	return "LERP_SCROLL_VERT";
+				case Transition.ScrollH:	return "LERP_SCROLL_HORIZ";
+				case Transition.Circle:		return "LERP_CIRCLE";
+				case Transition.Diamond:	return "LERP_DIAMOND";
+				case Transition.Blinds:		return "LERP_BLINDS";
+				case Transition.Arrows:		return "LERP_ARROW";
+				case Transition.SlideH:		return "LERP_SLIDE_HORIZ";
+				case Transition.SlideV:		return "LERP_SLIDE_VERT";
+				case Transition.Zoom:		return "LERP_ZOOM_FADE";
+				case Transition.RectV:		return "LERP_RECTS_VERT";
+			}
+			return string.Empty;
+		}
+
+#region Easing
+
+		/// <summary>
+		/// Easing functions
+		/// </summary>
+		[System.Serializable]
+		public class Easing
+		{
+			public Preset preset = Preset.Linear;
+
+			public enum Preset
+			{
+				Step,
+				Linear,
+				InQuad,
+				OutQuad,
+				InOutQuad,
+				InCubic,
+				OutCubic,
+				InOutCubic,
+				InQuint,
+				OutQuint,
+				InOutQuint,
+				InQuart,
+				OutQuart,
+				InOutQuart,
+				InExpo,
+				OutExpo,
+				InOutExpo,
+				Random,
+				RandomNotStep,
+			}
+
+			public static System.Func<float, float> GetFunction(Preset preset)
+			{
+				System.Func<float, float> result = null;
+				switch (preset)
+				{
+					case Preset.Step:
+						result = Step;
+						break;
+					case Preset.Linear:
+						result = Linear;
+						break;
+					case Preset.InQuad:
+						result = InQuad;
+						break;
+					case Preset.OutQuad:
+						result = OutQuad;
+						break;
+					case Preset.InOutQuad:
+						result = InOutQuad;
+						break;
+					case Preset.InCubic:
+						result = InCubic;
+						break;
+					case Preset.OutCubic:
+						result = OutCubic;
+						break;
+					case Preset.InOutCubic:
+						result = InOutCubic;
+						break;
+					case Preset.InQuint:
+						result = InQuint;
+						break;
+					case Preset.OutQuint:
+						result = OutQuint;
+						break;
+					case Preset.InOutQuint:
+						result = InOutQuint;
+						break;
+					case Preset.InQuart:
+						result = InQuart;
+						break;
+					case Preset.OutQuart:
+						result = OutQuart;
+						break;
+					case Preset.InOutQuart:
+						result = InOutQuart;
+						break;
+					case Preset.InExpo:
+						result = InExpo;
+						break;
+					case Preset.OutExpo:
+						result = OutExpo;
+						break;
+					case Preset.InOutExpo:
+						result = InOutExpo;
+						break;
+					case Preset.Random:
+						result = GetFunction((Preset)Random.Range(0, (int)Preset.Random));
+						break;
+					case Preset.RandomNotStep:
+						result = GetFunction((Preset)Random.Range((int)Preset.Step+1, (int)Preset.Random));
+						break;
+				}
+				return result;
+			}
+
+			public static float PowerEaseIn(float t, float power)
+			{
+				return Mathf.Pow(t, power);
+			}
+
+			public static float PowerEaseOut(float t, float power)
+			{
+				return 1f - Mathf.Abs(Mathf.Pow(t - 1f, power));
+			}
+
+			public static float PowerEaseInOut(float t, float power)
+			{
+				float result;
+				if (t < 0.5f)
+				{
+					result = PowerEaseIn(t * 2f, power) / 2f;
+				}
+				else
+				{
+					result = PowerEaseOut(t * 2f - 1f, power) / 2f + 0.5f;
+				}
+				return result;
+			}
+
+			public static float Step(float t)
+			{
+				float result = 0f;
+				if (t >= 0.5f)
+				{
+					result = 1f;
+				}
+				return result;
+			}
+
+			public static float Linear(float t)
+			{
+				return t;
+			}
+
+			public static float InQuad(float t)
+			{
+				return PowerEaseIn(t, 2f);
+			}
+
+			public static float OutQuad(float t)
+			{
+				return PowerEaseOut(t, 2f);
+				//return t * (2f - t);
+			}
+
+			public static float InOutQuad(float t)
+			{
+				return PowerEaseInOut(t, 2f);
+				//return t < 0.5 ? (2f * t * t) : (-1f + (4f - 2f * t) * t);
+			}
+
+			public static float InCubic(float t)
+			{
+				return PowerEaseIn(t, 3f);
+				//return t * t * t;
+			}
+
+			public static float OutCubic(float t)
+			{
+				return PowerEaseOut(t, 3f);
+				//return (--t) * t * t + 1f;
+			}
+
+			public static float InOutCubic(float t)
+			{
+				return PowerEaseInOut(t, 3f);
+				//return t < .5f ? (4f * t * t * t) : ((t - 1f) * (2f * t - 2f) * (2f * t - 2f) + 1f);
+			}
+
+			public static float InQuart(float t)
+			{
+				return PowerEaseIn(t, 4f);
+				//return t * t * t * t;
+			}
+
+			public static float OutQuart(float t)
+			{
+				return PowerEaseOut(t, 4f);
+				//return 1f - (--t) * t * t * t;
+			}
+
+			public static float InOutQuart(float t)
+			{
+				return PowerEaseInOut(t, 4f);
+				//return t < 0.5f ? (8f * t * t * t * t) : (1f - 8f * (--t) * t * t * t);
+			}
+
+			public static float InQuint(float t)
+			{
+				return PowerEaseIn(t, 5f);
+				//return t * t * t * t * t;
+			}
+
+			public static float OutQuint(float t)
+			{
+				return PowerEaseOut(t, 5f);
+				//return 1f + (--t) * t * t * t * t;
+			}
+
+			public static float InOutQuint(float t)
+			{
+				return PowerEaseInOut(t, 5f);
+				//return t < 0.5f ? (16f * t * t * t * t * t) : (1f + 16f * (--t) * t * t * t * t);
+			}
+
+			public static float InExpo(float t)
+			{
+				float result = 0f;
+				if (t != 0f)
+				{
+					result = Mathf.Pow(2f, 10f * (t - 1f));
+				}
+				return result;
+			}
+
+			public static float OutExpo(float t)
+			{
+				float result = 1f;
+				if (t != 1f)
+				{
+					result = -Mathf.Pow(2f, -10f * t) + 1f;
+				}
+				return result;
+			}
+
+			public static float InOutExpo(float t)
+			{
+				float result = 0f;
+				if (t > 0f)
+				{
+					result = 1f;
+					if (t < 1f)
+					{
+						t *= 2f;
+						if (t < 1f)
+						{
+							result = 0.5f * Mathf.Pow(2f, 10f * (t - 1f));
+						}
+						else
+						{
+							t--;
+							result = 0.5f * (-Mathf.Pow(2f, -10f * t) + 2f);
+						}
+					}
+				}
+				return result;
+			}
+		}
+#endregion Easing
+
+	}
+}

+ 17 - 0
Assets/AVProVideo/Runtime/Scripts/Components/PlaylistMediaPlayer.cs.meta

@@ -0,0 +1,17 @@
+fileFormatVersion: 2
+guid: e9ea31f33222f4b418e4e051a8a5ed24
+timeCreated: 1588679963
+licenseType: Pro
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences:
+  - m_AudioHeadTransform: {instanceID: 0}
+  - m_AudioFocusTransform: {instanceID: 0}
+  - _transitionShader: {fileID: 4800000, guid: 73f378cafe7b4a745907b70e76bb3259, type: 3}
+  - _playerA: {instanceID: 0}
+  - _playerB: {instanceID: 0}
+  executionOrder: 0
+  icon: {fileID: 2800000, guid: bb83b41b53a59874692b83eab5873998, type: 3}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 156 - 0
Assets/AVProVideo/Runtime/Scripts/Components/ResolveToRenderTexture.cs

@@ -0,0 +1,156 @@
+using UnityEngine;
+
+//-----------------------------------------------------------------------------
+// Copyright 2019-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	/// Renders the video texture to a RenderTexture - either one provided by the user (external) or to an internal one.
+	/// The video frames can optionally be "resolved" to unpack packed alpha, display a single stereo eye, generate mip maps, and apply colorspace conversions
+	[AddComponentMenu("AVPro Video/Resolve To RenderTexture", 330)]
+	[HelpURL("https://www.renderheads.com/products/avpro-video/")]
+	public class ResolveToRenderTexture : MonoBehaviour
+	{
+		[SerializeField] MediaPlayer _mediaPlayer = null;
+		[SerializeField] VideoResolveOptions _options = VideoResolveOptions.Create();
+		[SerializeField] VideoRender.ResolveFlags _resolveFlags = (VideoRender.ResolveFlags.ColorspaceSRGB | VideoRender.ResolveFlags.Mipmaps | VideoRender.ResolveFlags.PackedAlpha | VideoRender.ResolveFlags.StereoLeft);
+		[SerializeField] RenderTexture _externalTexture = null;
+
+		private Material _materialResolve;
+		private bool _isMaterialSetup;
+		private bool _isMaterialDirty;
+		private bool _isMaterialOES;
+		private RenderTexture _internalTexture;
+		private int _textureFrameCount = -1;
+
+		public MediaPlayer MediaPlayer
+		{
+			get
+			{
+				return _mediaPlayer;
+			}
+			set
+			{
+				ChangeMediaPlayer(value);
+			}
+		}
+
+		public RenderTexture ExternalTexture
+		{
+			get
+			{
+				return _externalTexture;
+			}
+			set
+			{
+				_externalTexture = value;
+			}
+		}
+
+		public RenderTexture TargetTexture
+		{
+			get
+			{
+				if (_externalTexture == null) return _internalTexture;
+				return _externalTexture;
+			}
+		}
+
+		public void SetMaterialDirty()
+		{
+			_isMaterialDirty = true;
+		}
+
+		private void ChangeMediaPlayer(MediaPlayer mediaPlayer)
+		{
+			if (_mediaPlayer != mediaPlayer)
+			{
+				_mediaPlayer = mediaPlayer;
+				_textureFrameCount = -1;
+				_isMaterialSetup = false;
+				_isMaterialDirty = true;
+				Resolve();
+			}
+		}
+
+		void Start()
+		{
+			_isMaterialOES = ( _mediaPlayer != null ) ? _mediaPlayer.IsUsingAndroidOESPath() : false;
+			_materialResolve = VideoRender.CreateResolveMaterial( _isMaterialOES );
+			VideoRender.SetupMaterialForMedia(_materialResolve, _mediaPlayer, -1);
+		}
+
+		void LateUpdate()
+		{
+			Debug.Assert(_mediaPlayer != null);
+			Resolve();
+		}
+
+		public void Resolve()
+		{
+			ITextureProducer textureProducer = _mediaPlayer != null ? _mediaPlayer.TextureProducer : null;
+			if (textureProducer != null && textureProducer.GetTexture())
+			{
+				// Check for a swap between OES and none-OES
+				bool playerIsOES = _mediaPlayer.IsUsingAndroidOESPath();
+				if ( _isMaterialOES != playerIsOES )
+				{
+					_isMaterialOES = playerIsOES;
+					_materialResolve = VideoRender.CreateResolveMaterial( playerIsOES );
+				}
+
+				if (!_isMaterialSetup)
+				{
+					VideoRender.SetupMaterialForMedia(_materialResolve, _mediaPlayer, -1);
+					_isMaterialSetup = true;
+					_isMaterialDirty = true;
+				}
+				if (_isMaterialDirty)
+				{
+					VideoRender.SetupResolveMaterial(_materialResolve, _options);
+					_isMaterialDirty = false;
+				}
+
+				int textureFrameCount = textureProducer.GetTextureFrameCount();
+				if (textureFrameCount != _textureFrameCount)
+				{
+					_internalTexture = VideoRender.ResolveVideoToRenderTexture(_materialResolve, _internalTexture, textureProducer, _resolveFlags);
+					_textureFrameCount = textureFrameCount;
+
+					if (_internalTexture && _externalTexture)
+					{
+						// NOTE: This blit can be removed once we can ResolveVideoToRenderTexture is made not to recreate textures
+						// NOTE: This blit probably doesn't do correct linear/srgb conversion if the colorspace settings differ, may have to use GL.sRGBWrite
+						Graphics.Blit(_internalTexture, _externalTexture);
+					}
+				}
+			}
+		}
+
+		void OnDisable()
+		{
+			if (_internalTexture)
+			{
+				RenderTexture.ReleaseTemporary(_internalTexture); _internalTexture = null;
+			}
+		}
+
+		void OnDestroy()
+		{
+			if (_materialResolve)
+			{
+				Destroy(_materialResolve); _materialResolve = null;
+			}
+		}
+#if false
+		void OnGUI()
+		{
+			if (TargetTexture)
+			{
+				GUI.DrawTexture(new Rect(0f, 0f, Screen.width * 0.8f, Screen.height * 0.8f), TargetTexture, ScaleMode.ScaleToFit, true);
+			}
+		}
+#endif
+	}
+}

+ 12 - 0
Assets/AVProVideo/Runtime/Scripts/Components/ResolveToRenderTexture.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 448e5e4039505584c852da1a7cc5c361
+timeCreated: 1654790987
+licenseType: Pro
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {fileID: 2800000, guid: bb83b41b53a59874692b83eab5873998, type: 3}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 178 - 0
Assets/AVProVideo/Runtime/Scripts/Components/UpdateMultiPassStereo.cs

@@ -0,0 +1,178 @@
+#if UNITY_ANDROID
+	#if USING_URP
+		#define ANDROID_URP
+	#endif
+#endif
+
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.Rendering;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	/// <summary>
+	/// This script is needed to send the camera position to the stereo shader so that
+	/// it can determine which eye it is rendering.  This is only needed for multi-pass
+	/// rendering, as single pass has a built-in shader variable
+	/// </summary>
+	[AddComponentMenu("AVPro Video/Update Multi-Pass Stereo", 320)]
+	[HelpURL("https://www.renderheads.com/products/avpro-video/")]
+	public class UpdateMultiPassStereo : MonoBehaviour
+	{
+		[Header("Stereo camera")]
+		[SerializeField] Camera _camera = null;
+
+		public Camera Camera
+		{
+			get { return _camera; }
+			set { _camera = value; }
+		}
+
+		private static readonly LazyShaderProperty PropWorldCameraPosition = new LazyShaderProperty("_WorldCameraPosition");
+		private static readonly LazyShaderProperty PropWorldCameraRight = new LazyShaderProperty("_WorldCameraRight");
+
+		// State
+
+		private Camera _foundCamera;
+
+		void Awake()
+		{
+			if (_camera == null)
+			{
+				Debug.LogWarning("[AVProVideo] No camera set for UpdateMultiPassStereo component. If you are rendering in multi-pass stereo then it is recommended to set this.");
+			}
+		}
+
+		void Start()
+		{
+			LogXRDeviceDetails();
+
+			#if ANDROID_URP
+				if( GetComponent<Camera>() == null )
+				{
+					throw new MissingComponentException("[AVProVideo] When using URP the UpdateMultiPassStereo component must be on the Camera gameobject. This component is not required on all VR devices, but if it is then stereo eye rendering may not work correctly.");
+				}
+			#endif
+		}
+
+		private void LogXRDeviceDetails()
+		{
+#if UNITY_2019_1_OR_NEWER && !UNITY_TVOS
+			string logOutput = "[AVProVideo] XR Device details: UnityEngine.XR.XRSettings.loadedDeviceName = " + UnityEngine.XR.XRSettings.loadedDeviceName + " | supportedDevices = ";
+
+			string[] aSupportedDevices = UnityEngine.XR.XRSettings.supportedDevices;
+			int supportedDeviceCount = aSupportedDevices.Length;
+			for (int i = 0; i < supportedDeviceCount; i++)
+			{
+				logOutput += aSupportedDevices[i];
+				if( i < (supportedDeviceCount - 1 ))
+				{
+					logOutput += ", ";
+				}
+			}
+
+			List<UnityEngine.XR.InputDevice> inputDevices = new List<UnityEngine.XR.InputDevice>();
+			UnityEngine.XR.InputDevices.GetDevices(inputDevices);
+			int deviceCount = inputDevices.Count;
+			if (deviceCount > 0)
+			{
+				logOutput += " | XR Devices = ";
+
+				for (int i = 0; i < deviceCount; i++)
+				{
+					logOutput += inputDevices[i].name;
+					if( i < (deviceCount -1 ))
+					{
+						logOutput += ", ";
+					}
+				}
+			}
+
+			UnityEngine.XR.InputDevice headDevice = UnityEngine.XR.InputDevices.GetDeviceAtXRNode(UnityEngine.XR.XRNode.Head);
+			if( headDevice != null )
+			{
+				logOutput += " | headDevice name = " + headDevice.name + ", manufacturer = " + headDevice.manufacturer;
+			}
+
+			Debug.Log(logOutput);
+#endif
+		}
+
+
+#if ANDROID_URP
+		void OnEnable()
+		{
+			RenderPipelineManager.beginCameraRendering += RenderPipelineManager_beginCameraRendering;
+		}
+		void OnDisable()
+		{
+			RenderPipelineManager.beginCameraRendering -= RenderPipelineManager_beginCameraRendering;
+		}
+#endif
+
+		private static bool IsMultiPassVrEnabled()
+		{
+		#if UNITY_TVOS
+			return false;
+		#else
+			#if UNITY_2017_2_OR_NEWER
+			if (!UnityEngine.XR.XRSettings.enabled) return false;
+			#endif
+			#if UNITY_2018_3_OR_NEWER
+			if (UnityEngine.XR.XRSettings.stereoRenderingMode != UnityEngine.XR.XRSettings.StereoRenderingMode.MultiPass) return false;
+			#endif
+			return true;
+		#endif
+		}
+
+
+		// We do a LateUpdate() to allow for any changes in the camera position that may have happened in Update()
+#if ANDROID_URP
+		// Android URP
+		private void RenderPipelineManager_beginCameraRendering(ScriptableRenderContext context, Camera camera)
+#else
+		// Normal render pipeline
+		private void LateUpdate()
+#endif
+		{
+			if (!IsMultiPassVrEnabled())
+			{
+				return;
+			}
+
+			if (_camera != null && _foundCamera != _camera)
+			{
+				_foundCamera = _camera;
+			}
+			if (_foundCamera == null)
+			{
+				_foundCamera = Camera.main;
+				if (_foundCamera == null)
+				{
+					Debug.LogWarning("[AVProVideo] Cannot find main camera for UpdateMultiPassStereo, this can lead to eyes flickering");
+					if (Camera.allCameras.Length > 0)
+					{
+						_foundCamera = Camera.allCameras[0];
+						Debug.LogWarning("[AVProVideo] UpdateMultiPassStereo using camera " + _foundCamera.name);
+					}
+				}
+			}
+
+			if (_foundCamera != null)
+			{
+				#if ANDROID_URP
+					Shader.EnableKeyword("USING_URP");
+				#else
+					Shader.DisableKeyword("USING_URP");
+				#endif
+
+				Shader.SetGlobalVector(PropWorldCameraPosition.Id, _foundCamera.transform.position);
+				Shader.SetGlobalVector(PropWorldCameraRight.Id, _foundCamera.transform.right);
+			}
+		}
+	}
+}

+ 12 - 0
Assets/AVProVideo/Runtime/Scripts/Components/UpdateMultiPassStereo.cs.meta

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

+ 5 - 0
Assets/AVProVideo/Runtime/Scripts/Internal.meta

@@ -0,0 +1,5 @@
+fileFormatVersion: 2
+guid: 1bb8f28c4529a1343b4430d732bb5f2a
+folderAsset: yes
+DefaultImporter:
+  userData: 

+ 124 - 0
Assets/AVProVideo/Runtime/Scripts/Internal/ApplyToBase.cs

@@ -0,0 +1,124 @@
+#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS || UNITY_TVOS
+	#define UNITY_PLATFORM_SUPPORTS_YPCBCR
+#endif
+
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace RenderHeads.Media.AVProVideo
+{
+	/// <summary>
+	/// Base class to apply texture from MediaPlayer
+	/// </summary>
+	public abstract class ApplyToBase : MonoBehaviour
+	{
+		[Header("Media Source")]
+		[Space(8f)]
+
+		[SerializeField] protected MediaPlayer _media = null;
+
+		public MediaPlayer Player
+		{
+			get { return _media; }
+			set { ChangeMediaPlayer(value); }
+		}
+
+		[Space(8f)]
+		[Header("Display")]
+
+		[SerializeField] bool _automaticStereoPacking = true;
+		public bool AutomaticStereoPacking
+		{
+			get { return _automaticStereoPacking; }
+			set { if (_automaticStereoPacking != value) { _automaticStereoPacking = value; _isDirty = true; } }
+		}
+
+		[SerializeField] StereoPacking _overrideStereoPacking = StereoPacking.None;
+		public StereoPacking OverrideStereoPacking
+		{
+			get { return _overrideStereoPacking; }
+			set { if (_overrideStereoPacking != value) { _overrideStereoPacking = value; _isDirty = true; } }
+		}
+
+		[SerializeField] bool _stereoRedGreenTint = false;
+		public bool StereoRedGreenTint { get { return _stereoRedGreenTint; } set { if (_stereoRedGreenTint != value) { _stereoRedGreenTint = value; _isDirty = true; } } }
+
+		protected bool _isDirty = false;
+
+		void Awake()
+		{
+			ChangeMediaPlayer(_media, force:true);
+		}
+
+		private void ChangeMediaPlayer(MediaPlayer player, bool force = false)
+		{
+			if (_media != player || force)
+			{
+				if (_media != null)
+				{
+					_media.Events.RemoveListener(OnMediaPlayerEvent);
+				}
+				_media = player;
+				if (_media != null)
+				{
+					_media.Events.AddListener(OnMediaPlayerEvent);
+				}
+				_isDirty = true;
+			}
+		}
+
+		// Callback function to handle events
+		private void OnMediaPlayerEvent(MediaPlayer mp, MediaPlayerEvent.EventType et, ErrorCode errorCode)
+		{
+			switch (et)
+			{
+				case MediaPlayerEvent.EventType.FirstFrameReady:
+				case MediaPlayerEvent.EventType.PropertiesChanged:
+					ForceUpdate();
+					break;
+			}
+		}
+
+		public void ForceUpdate()
+		{
+			_isDirty = true;
+			if (this.isActiveAndEnabled)
+			{
+				Apply();
+			}
+		}
+
+		private void Start()
+		{
+			SaveProperties();
+			Apply();
+		}
+
+		protected virtual void OnEnable()
+		{
+			SaveProperties();
+			ForceUpdate();
+		}
+
+		protected virtual void OnDisable()
+		{
+			RestoreProperties();
+		}
+
+		private void OnDestroy()
+		{
+			ChangeMediaPlayer(null);
+		}
+
+		protected virtual void SaveProperties()
+		{
+		}
+
+		protected virtual void RestoreProperties()
+		{
+		}
+
+		public abstract void Apply();
+	}
+}

+ 11 - 0
Assets/AVProVideo/Runtime/Scripts/Internal/ApplyToBase.cs.meta

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

+ 215 - 0
Assets/AVProVideo/Runtime/Scripts/Internal/AudioOutputManager.cs

@@ -0,0 +1,215 @@
+using System.Collections.Generic;
+using UnityEngine;
+using System;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	/// <summary>
+	/// A singleton to handle multiple instances of the AudioOutput component
+	/// </summary>
+	public class AudioOutputManager
+	{
+		private static AudioOutputManager _instance = null;
+
+		public static AudioOutputManager Instance
+		{
+			get
+			{
+				if (_instance == null)
+				{
+					_instance = new AudioOutputManager();
+				}
+				return _instance;
+			}
+		}
+
+		protected class PlayerInstance
+		{
+			public HashSet<AudioOutput> outputs;
+			public float[] pcmData;
+			public bool isPcmDataReady;
+		}
+
+		private Dictionary<MediaPlayer, PlayerInstance> _instances;
+
+		private AudioOutputManager()
+		{
+			_instances = new Dictionary<MediaPlayer, PlayerInstance>();
+		}
+
+		public void RequestAudio(AudioOutput outputComponent, MediaPlayer mediaPlayer, float[] audioData, int audioChannelCount, int channelMask, AudioOutput.AudioOutputMode audioOutputMode, bool supportPositionalAudio)
+		{
+			if (mediaPlayer == null || mediaPlayer.Control == null || !mediaPlayer.Control.IsPlaying())
+			{
+				if (supportPositionalAudio)
+				{
+					ZeroAudio(audioData, 0);
+				}
+				return;
+			}
+
+			int channels = mediaPlayer.Control.GetAudioChannelCount();
+			if (channels <= 0)
+			{
+				if (supportPositionalAudio)
+				{
+					ZeroAudio(audioData, 0);
+				}
+				return;
+			}
+
+			// total samples requested should be multiple of channels
+			Debug.Assert(audioData.Length % audioChannelCount == 0);
+
+			// Find or create an instance
+			PlayerInstance instance = null;
+			if (!_instances.TryGetValue(mediaPlayer, out instance))
+			{
+				instance = _instances[mediaPlayer] = new PlayerInstance()
+				{
+					outputs = new HashSet<AudioOutput>(),
+					pcmData = null
+				};
+			}
+
+			// requests data if it hasn't been requested yet for the current cycle
+			if (instance.outputs.Count == 0 || instance.outputs.Contains(outputComponent) || instance.pcmData == null)
+			{
+				instance.outputs.Clear();
+
+				int actualDataRequired = (audioData.Length * channels) / audioChannelCount;
+				if (instance.pcmData == null || actualDataRequired != instance.pcmData.Length)
+				{
+					instance.pcmData = new float[actualDataRequired];
+				}
+
+				instance.isPcmDataReady = GrabAudio(mediaPlayer, instance.pcmData, channels);
+
+				instance.outputs.Add(outputComponent);
+			}
+
+			if (instance.isPcmDataReady)
+			{
+				// calculate how many samples and what channels are needed and then copy over the data
+				int samples = Math.Min(audioData.Length / audioChannelCount, instance.pcmData.Length / channels);
+				int storedPos = 0;
+				int requestedPos = 0;
+
+				// multiple mode, copies over audio from desired channels into the same channels on the audiosource
+				if (audioOutputMode == AudioOutput.AudioOutputMode.MultipleChannels)
+				{
+					int lesserChannels = Math.Min(channels, audioChannelCount);
+
+					if (!supportPositionalAudio)
+					{
+						for (int i = 0; i < samples; ++i)
+						{
+							for (int j = 0; j < lesserChannels; ++j)
+							{
+								if ((1 << j & channelMask) > 0)
+								{
+									audioData[requestedPos + j] = instance.pcmData[storedPos + j];
+								}
+							}
+
+							storedPos += channels;
+							requestedPos += audioChannelCount;
+						}
+					}
+					else
+					{
+						for (int i = 0; i < samples; ++i)
+						{
+							for (int j = 0; j < lesserChannels; ++j)
+							{
+								if ((1 << j & channelMask) > 0)
+								{
+									audioData[requestedPos + j] *= instance.pcmData[storedPos + j];
+								}
+							}
+
+							storedPos += channels;
+							requestedPos += audioChannelCount;
+						}
+					}
+				}
+				//Mono mode, copies over single channel to all output channels
+				else if (audioOutputMode == AudioOutput.AudioOutputMode.OneToAllChannels)
+				{
+					int desiredChannel = 0;
+
+					for (int i = 0; i < 8; ++i)
+					{
+						if ((channelMask & (1 << i)) > 0)
+						{
+							desiredChannel = i;
+							break;
+						}
+					}
+
+					if (desiredChannel < channels)
+					{
+						if (!supportPositionalAudio)
+						{
+							for (int i = 0; i < samples; ++i)
+							{
+								for (int j = 0; j < audioChannelCount; ++j)
+								{
+									audioData[requestedPos + j] = instance.pcmData[storedPos + desiredChannel];
+								}
+
+								storedPos += channels;
+								requestedPos += audioChannelCount;
+							}
+						}
+						else
+						{
+							for (int i = 0; i < samples; ++i)
+							{
+								for (int j = 0; j < audioChannelCount; ++j)
+								{
+									audioData[requestedPos + j] *= instance.pcmData[storedPos + desiredChannel];
+								}
+
+								storedPos += channels;
+								requestedPos += audioChannelCount;
+							}
+						}
+					}
+				}
+
+				// If there is left over audio
+				if (supportPositionalAudio && requestedPos != audioData.Length)
+				{
+					// Zero the remaining audio data otherwise there are pops
+					ZeroAudio(audioData, requestedPos);
+				}
+			}
+			else
+			{
+				if (supportPositionalAudio)
+				{
+					// Zero the remaining audio data otherwise there are pops
+					ZeroAudio(audioData, 0);
+				}
+			}
+		}
+
+		private void ZeroAudio(float[] audioData, int startPosition)
+		{
+			for (int i = startPosition; i < audioData.Length; i++)
+			{
+				audioData[i] = 0f;
+			}
+		}
+
+		private bool GrabAudio(MediaPlayer player, float[] audioData, int channelCount)
+		{
+			return (0 != player.Control.GrabAudio(audioData, audioData.Length, channelCount));
+		}
+	}
+}

+ 12 - 0
Assets/AVProVideo/Runtime/Scripts/Internal/AudioOutputManager.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 714026a371bd2d64c86edb3dab5607d9
+timeCreated: 1495699104
+licenseType: Free
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 652 - 0
Assets/AVProVideo/Runtime/Scripts/Internal/BaseMediaPlayer.cs

@@ -0,0 +1,652 @@
+#if UNITY_EDITOR || UNITY_STANDALONE_OSX || UNITY_STANDALONE_WIN || UNITY_IOS || UNITY_ANDROID
+	#define UNITY_PLATFORM_SUPPORTS_LINEAR
+#endif
+
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	/// <summary>
+	/// Base class for all platform specific MediaPlayers
+	/// </summary>
+	public abstract partial class BaseMediaPlayer : IMediaPlayer, IMediaControl, IMediaInfo, IMediaCache, ITextureProducer, IMediaSubtitles, IVideoTracks, IAudioTracks, ITextTracks, IBufferedDisplay, System.IDisposable
+	{
+		public BaseMediaPlayer()
+		{
+			InitTracks();
+		}
+
+		public abstract string		GetVersion();
+		public abstract string		GetExpectedVersion();
+
+		/// <inheritdoc/>
+		public abstract bool		OpenMedia(string path, long offset, string customHttpHeaders, MediaHints mediaHints, int forceFileFormat = 0, bool startWithHighestBitrate = false);
+
+#if NETFX_CORE
+		/// <inheritdoc/>
+		public virtual bool			OpenMedia(Windows.Storage.Streams.IRandomAccessStream ras, string path, long offset, string customHttpHeaders) { return false; }
+#endif
+
+		/// <inheritdoc/>
+		public virtual bool			OpenMediaFromBuffer(byte[] buffer) { return false; }
+		/// <inheritdoc/>
+		public virtual bool			StartOpenMediaFromBuffer(ulong length) { return false; }
+		/// <inheritdoc/>
+		public virtual bool			AddChunkToMediaBuffer(byte[] chunk, ulong offset, ulong length) { return false; }
+		/// <inheritdoc/>
+		public virtual bool			EndOpenMediaFromBuffer() { return false; }
+
+		/// <inheritdoc/>
+		public virtual void			CloseMedia()
+		{
+			#if UNITY_EDITOR
+			_displayRateLastRealTime = 0f;
+			#endif
+			_displayRateTimer = 0f;
+			_displayRateLastFrameCount = 0;
+			_displayRate = 0f;
+
+			_stallDetectionTimer = 0f;
+			_stallDetectionFrame = 0;
+			_lastError = ErrorCode.None;
+
+			_textTracks.Clear();
+			_audioTracks.Clear();
+			_videoTracks.Clear();
+			_currentTextCue = null;
+			_mediaHints = new MediaHints();
+		}
+
+		/// <inheritdoc/>
+		public abstract void		SetLooping(bool looping);
+		/// <inheritdoc/>
+		public abstract bool		IsLooping();
+
+		/// <inheritdoc/>
+		public abstract bool		HasMetaData();
+		/// <inheritdoc/>
+		public abstract bool		CanPlay();
+		/// <inheritdoc/>
+		public abstract void		Play();
+		/// <inheritdoc/>
+		public abstract void		Pause();
+		/// <inheritdoc/>
+		public abstract void		Stop();
+		/// <inheritdoc/>
+		public virtual void			Rewind() { SeekFast(0.0);  }
+
+		/// <inheritdoc/>
+		public abstract void		Seek(double time);
+		/// <inheritdoc/>
+		public abstract void		SeekFast(double time);
+		/// <inheritdoc/>
+		public virtual void			SeekWithTolerance(double time, double timeDeltaBefore, double timeDeltaAfter) { Seek(time); }
+		/// <inheritdoc/>
+		public abstract double		GetCurrentTime();
+		/// <inheritdoc/>
+		public virtual DateTime		GetProgramDateTime() { return DateTime.MinValue; }
+		/// <inheritdoc/>
+		public abstract float		GetPlaybackRate();
+		/// <inheritdoc/>
+		public abstract void		SetPlaybackRate(float rate);
+
+		// Basic Properties
+		/// <inheritdoc/>
+		public abstract double		GetDuration();
+		/// <inheritdoc/>
+		public abstract int			GetVideoWidth();
+		/// <inheritdoc/>
+		public abstract int			GetVideoHeight();
+		/// <inheritdoc/>
+		public abstract float		GetVideoFrameRate();
+		/// <inheritdoc/>
+		public virtual float		GetVideoDisplayRate() { return _displayRate; }
+		/// <inheritdoc/>
+		public abstract bool		HasAudio();
+		/// <inheritdoc/>
+		public abstract bool		HasVideo();
+		/// <inheritdoc/>
+		public bool 				IsVideoStereo() { return GetTextureStereoPacking() != StereoPacking.None; }
+
+		// Basic State
+		/// <inheritdoc/>
+		public abstract bool		IsSeeking();
+		/// <inheritdoc/>
+		public abstract bool		IsPlaying();
+		/// <inheritdoc/>
+		public abstract bool		IsPaused();
+		/// <inheritdoc/>
+		public abstract bool		IsFinished();
+		/// <inheritdoc/>
+		public abstract bool		IsBuffering();
+		/// <inheritdoc/>
+		public virtual bool			WaitForNextFrame(Camera dummyCamera, int previousFrameCount) { return false; }
+
+		// Textures
+		/// <inheritdoc/>
+		public virtual int			GetTextureCount() { return 1; }
+		/// <inheritdoc/>
+		public abstract Texture		GetTexture(int index = 0);
+		/// <inheritdoc/>
+		public abstract int			GetTextureFrameCount();
+		/// <inheritdoc/>
+		public virtual bool			SupportsTextureFrameCount() { return true; }
+		/// <inheritdoc/>
+		public virtual long			GetTextureTimeStamp() { return long.MinValue; }
+		/// <inheritdoc/>
+		public abstract bool		RequiresVerticalFlip();
+		/// <inheritdoc/>
+		public virtual float[]		GetTextureTransform() { return new float[] { 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f }; }
+		/// <inheritdoc/>
+		public virtual float		GetTexturePixelAspectRatio() { return 1f; }
+		/// <inheritdoc/>
+		public virtual Matrix4x4	GetYpCbCrTransform() { return Matrix4x4.identity; }
+
+		public StereoPacking GetTextureStereoPacking()
+		{
+			StereoPacking result = InternalGetTextureStereoPacking();
+			if (result == StereoPacking.Unknown)
+			{
+				// If stereo is unknown, fall back to media hints or no packing
+				result = _mediaHints.stereoPacking;
+			}
+			return result;
+		}
+		internal abstract StereoPacking InternalGetTextureStereoPacking();
+
+		public virtual TransparencyMode GetTextureTransparency()
+		{
+			return _mediaHints.transparency;
+		}
+
+		public AlphaPacking GetTextureAlphaPacking()
+		{
+			if (GetTextureTransparency() == TransparencyMode.Transparent)
+			{
+				return _mediaHints.alphaPacking;
+			}
+			return AlphaPacking.None;
+		}
+
+		// Audio General
+		/// <inheritdoc/>
+		public abstract void		MuteAudio(bool bMuted);
+		/// <inheritdoc/>
+		public abstract bool		IsMuted();
+		/// <inheritdoc/>
+		public abstract void		SetVolume(float volume);
+		/// <inheritdoc/>
+		public virtual void			SetBalance(float balance) { }
+		/// <inheritdoc/>
+		public abstract float		GetVolume();
+		/// <inheritdoc/>
+		public virtual float		GetBalance() { return 0f; }
+
+		// Audio Grabbing
+		/// <inheritdoc/>
+		public virtual int						GetAudioChannelCount() { return -1; }
+		/// <inheritdoc/>
+		public virtual AudioChannelMaskFlags	GetAudioChannelMask() { return 0; }
+		/// <inheritdoc/>
+		public virtual int	 					GrabAudio(float[] audioData, int audioDataFloatCount, int channelCount) { return 0; }
+		/// <inheritdoc/>
+		public virtual int	 					GetAudioBufferedSampleCount() { return 0; }
+
+		/// <inheritdoc/>
+		public virtual void AudioConfigurationChanged(bool deviceChanged) { }
+
+		// 360 Audio
+		/// <inheritdoc/>
+		public virtual void			SetAudioHeadRotation(Quaternion q) { }
+		/// <inheritdoc/>
+		public virtual void			ResetAudioHeadRotation() { }
+		/// <inheritdoc/>
+		public virtual void			SetAudioChannelMode(Audio360ChannelMode channelMode) { }
+		/// <inheritdoc/>
+		public virtual void			SetAudioFocusEnabled(bool enabled) { }
+		/// <inheritdoc/>
+		public virtual void			SetAudioFocusProperties(float offFocusLevel, float widthDegrees) { }
+		/// <inheritdoc/>
+		public virtual void			SetAudioFocusRotation(Quaternion q) { }
+		/// <inheritdoc/>
+		public virtual void			ResetAudioFocus() { }
+
+		// Streaming
+		/// <inheritdoc/>
+		public virtual long			GetEstimatedTotalBandwidthUsed() { return -1; }
+		/// <inheritdoc/>
+		public virtual void			SetPlayWithoutBuffering(bool playWithoutBuffering) { }
+
+		// Caching
+		/// <inheritdoc/>
+		public virtual bool					IsMediaCachingSupported() { return false; }
+		/// <inheritdoc/>
+		public virtual void					AddMediaToCache(string url, string headers, MediaCachingOptions options) { }
+		/// <inheritdoc/>
+		public virtual void					CancelDownloadOfMediaToCache(string url) { }
+		/// <inheritdoc/>
+		public virtual void					PauseDownloadOfMediaToCache(string url) { }
+		/// <inheritdoc/>
+		public virtual void					ResumeDownloadOfMediaToCache(string url) { }
+		/// <inheritdoc/>
+		public virtual void					RemoveMediaFromCache(string url) { }
+		/// <inheritdoc/>
+		public virtual CachedMediaStatus	GetCachedMediaStatus(string url, ref float progress) { return CachedMediaStatus.NotCached; }
+//		/// <inheritdoc/>
+//		public virtual bool					IsMediaCached() { return false; }
+
+		// External playback
+		/// <inheritdoc/>
+		public virtual bool			IsExternalPlaybackSupported() { return false; }
+		/// <inheritdoc/>
+		public virtual bool			IsExternalPlaybackActive() { return false; }
+		/// <inheritdoc/>
+		public virtual void			SetAllowsExternalPlayback(bool enable) { }
+		/// <inheritdoc/>
+		public virtual void			SetExternalPlaybackVideoGravity(ExternalPlaybackVideoGravity gravity) { }
+
+		// Authentication
+		//public virtual void			SetKeyServerURL(string url) { }
+		/// <inheritdoc/>
+		public virtual void			SetKeyServerAuthToken(string token) { }
+		/// <inheritdoc/>
+		public virtual void			SetOverrideDecryptionKey(byte[] key) { }
+
+		// General
+		/// <inheritdoc/>
+		public abstract void		Update();
+		/// <inheritdoc/>
+		public abstract void		Render();
+		/// <inheritdoc/>
+		public abstract void		Dispose();
+
+		// Internal method
+		public virtual bool GetDecoderPerformance(ref int activeDecodeThreadCount, ref int decodedFrameCount, ref int droppedFrameCount) { return false; }
+
+#if false
+		public void Update()
+		{
+			Native.Update(_instance);
+			if (UpdateTracks())
+			{
+
+			}
+			if (UpdateTextCue())
+			{
+
+			}
+		}
+#endif
+
+		public virtual void EndUpdate() { }
+
+		public virtual IntPtr GetNativePlayerHandle() { return IntPtr.Zero; }
+
+		public ErrorCode GetLastError()
+		{
+			ErrorCode errorCode = _lastError;
+			_lastError = ErrorCode.None;
+			return errorCode;
+		}
+
+		/// <inheritdoc/>
+		public virtual long GetLastExtendedErrorCode()
+		{
+			return 0;
+		}
+
+		public string GetPlayerDescription()
+		{
+			return _playerDescription;
+		}
+
+		/// <inheritdoc/>
+		public virtual bool PlayerSupportsLinearColorSpace()
+		{
+#if UNITY_PLATFORM_SUPPORTS_LINEAR
+			return true;
+#else
+			return false;
+#endif
+		}
+
+		protected string _playerDescription = string.Empty;
+		protected ErrorCode _lastError = ErrorCode.None;
+		protected FilterMode _defaultTextureFilterMode = FilterMode.Bilinear;
+		protected TextureWrapMode _defaultTextureWrapMode = TextureWrapMode.Clamp;
+		protected int _defaultTextureAnisoLevel = 1;
+		protected MediaHints _mediaHints;
+		protected TimeRanges _seekableTimes = new TimeRanges();
+		protected TimeRanges _bufferedTimes = new TimeRanges();
+
+		public TimeRanges GetSeekableTimes() { return _seekableTimes; }
+		public TimeRanges GetBufferedTimes() { return _bufferedTimes; }
+
+		public void GetTextureProperties(out FilterMode filterMode, out TextureWrapMode wrapMode, out int anisoLevel)
+		{
+			filterMode = _defaultTextureFilterMode;
+			wrapMode = _defaultTextureWrapMode;
+			anisoLevel = _defaultTextureAnisoLevel;
+		}
+
+		public void SetTextureProperties(FilterMode filterMode = FilterMode.Bilinear, TextureWrapMode wrapMode = TextureWrapMode.Clamp, int anisoLevel = 0)
+		{
+			_defaultTextureFilterMode = filterMode;
+			_defaultTextureWrapMode = wrapMode;
+			_defaultTextureAnisoLevel = anisoLevel;
+			for (int i = 0; i < GetTextureCount(); ++i)
+			{
+				ApplyTextureProperties(GetTexture(i));
+			}
+		}
+
+		protected virtual void ApplyTextureProperties(Texture texture)
+		{
+			if (texture != null)
+			{
+				texture.filterMode = _defaultTextureFilterMode;
+				texture.wrapMode = _defaultTextureWrapMode;
+				texture.anisoLevel = _defaultTextureAnisoLevel;
+			}
+		}
+
+#region Video Display Rate
+#if UNITY_EDITOR
+		private float 		_displayRateLastRealTime = 0f;
+#endif
+		private float		_displayRateTimer;
+		private int			_displayRateLastFrameCount;
+		private float		_displayRate = 1f;
+
+		protected void UpdateDisplayFrameRate()
+		{
+			const float IntervalSeconds = 0.5f;
+			if (_displayRateTimer >= IntervalSeconds)
+			{
+				int frameCount = GetTextureFrameCount();
+				int frameDelta = (frameCount - _displayRateLastFrameCount);
+				_displayRate = (float)frameDelta / _displayRateTimer;
+				_displayRateTimer -= IntervalSeconds;
+				if (_displayRateTimer >= IntervalSeconds) _displayRateTimer -= IntervalSeconds;
+				if (_displayRateTimer >= IntervalSeconds) _displayRateTimer = 0f;
+				_displayRateLastFrameCount = frameCount;
+			}
+
+			float deltaTime = Time.deltaTime;
+#if UNITY_EDITOR
+			if (!Application.isPlaying)
+			{
+				// When not playing Time.deltaTime isn't valid so we have to derive it
+				deltaTime = (Time.realtimeSinceStartup - _displayRateLastRealTime);
+				_displayRateLastRealTime = Time.realtimeSinceStartup;
+			}
+#endif
+			_displayRateTimer += deltaTime;
+		}
+#endregion	// Video Display Rate
+
+#region Stall Detection
+		protected bool IsExpectingNewVideoFrame()
+		{
+			if (HasVideo())
+			{
+				// If we're playing then we expect a new frame
+				if (!IsFinished() && (!IsPaused() && IsPlaying() && GetPlaybackRate() != 0.0f))
+				{
+					// Check that the video is not a single frame and therefore there is no other frame to display
+					bool isSingleFrame = (GetTextureFrameCount() > 0 && GetDurationFrames() == 1);
+					if (!isSingleFrame)
+					{
+						// NOTE: if a new frame isn't available then we could either be seeking or stalled
+						return true;
+					}
+				}
+			}
+			return false;
+		}
+
+		/// <inheritdoc/>
+		public virtual bool IsPlaybackStalled()
+		{
+			const float StallDetectionDuration = 0.5f;
+
+			// Manually detect stalled video if the platform doesn't have native support to detect it
+			if (SupportsTextureFrameCount() && IsExpectingNewVideoFrame())
+			{
+				// Detect a new video frame
+				int frameCount = GetTextureFrameCount();
+				if (frameCount != _stallDetectionFrame)
+				{
+					_stallDetectionTimer = 0f;
+					_stallDetectionFrame = frameCount;
+				}
+				else
+				{
+					// Update the detection timer, but never more than once a Unity frame
+					if (_stallDetectionGuard != Time.frameCount)
+					{
+						_stallDetectionTimer += Time.deltaTime;
+					}
+				}
+				_stallDetectionGuard = Time.frameCount;
+
+				float thresholdDuration = StallDetectionDuration;
+
+				// Scale by the playback rate, but should be at least StallDetectionDuration
+				thresholdDuration = Mathf.Max(thresholdDuration / Mathf.Abs(GetPlaybackRate()), StallDetectionDuration);
+
+				// If a valid FPS is available then make sure the thresholdDuration
+				// is at least double that.  This is mainly for very low FPS
+				// content (eg 1 or 2 FPS)
+				float fps = GetVideoFrameRate();
+				if (fps > 0f && !float.IsNaN(fps))
+				{
+					thresholdDuration = Mathf.Max(thresholdDuration, 2f / fps);
+				}
+
+				return (_stallDetectionTimer > thresholdDuration);
+			}
+			else
+			{
+				_stallDetectionTimer = 0f;
+			}
+			return false;
+		}
+
+		private float _stallDetectionTimer;
+		private int _stallDetectionFrame;
+		private int _stallDetectionGuard;
+#endregion // Stall Detection
+
+		protected List<Subtitle> _subtitles;
+		protected Subtitle _currentSubtitle;
+
+		/// <inheritdoc/>
+		public bool LoadSubtitlesSRT(string data)
+		{
+			if (string.IsNullOrEmpty(data))
+			{
+				// Disable subtitles
+				_subtitles = null;
+				_currentSubtitle = null;
+			}
+			else
+			{
+				_subtitles = SubtitleUtils.ParseSubtitlesSRT(data);
+				_currentSubtitle = null;
+			}
+			return (_subtitles != null);
+		}
+
+		/// <inheritdoc/>
+		public virtual void UpdateSubtitles()
+		{
+			if (_subtitles != null)
+			{
+				double time = GetCurrentTime();
+
+				// TODO: implement a more efficient subtitle index searcher
+				int searchIndex = 0;
+				if (_currentSubtitle != null)
+				{
+					if (!_currentSubtitle.IsTime(time))
+					{
+						if (time > _currentSubtitle.timeEnd)
+						{
+							searchIndex = _currentSubtitle.index + 1;
+						}
+						_currentSubtitle = null;
+					}
+				}
+
+				if (_currentSubtitle == null)
+				{
+					for (int i = searchIndex; i < _subtitles.Count; i++)
+					{
+						if (_subtitles[i].IsTime(time))
+						{
+							_currentSubtitle = _subtitles[i];
+							break;
+						}
+					}
+				}
+			}
+		}
+
+		/// <inheritdoc/>
+		public virtual int GetSubtitleIndex()
+		{
+			int result = -1;
+			if (_currentSubtitle != null)
+			{
+				result = _currentSubtitle.index;
+			}
+			return result;
+		}
+
+		/// <inheritdoc/>
+		public virtual string GetSubtitleText()
+		{
+			string result = string.Empty;
+			if (_currentSubtitle != null)
+			{
+				result = _currentSubtitle.text;
+			}
+			else if (_currentTextCue != null)
+			{
+				result = _currentTextCue.Text;
+			}
+			return result;
+		}
+
+		public virtual void OnEnable()
+		{
+		}
+
+		/// <inheritdoc/>
+		public int GetCurrentTimeFrames(float overrideFrameRate = 0f)
+		{
+			int result = 0;
+			float frameRate = (overrideFrameRate > 0f)?overrideFrameRate:GetVideoFrameRate();
+			if (frameRate > 0f)
+			{
+				result = Helper.ConvertTimeSecondsToFrame(GetCurrentTime(), frameRate);
+				result = Mathf.Min(result, GetMaxFrameNumber());
+			}
+			return result;
+		}
+
+		/// <inheritdoc/>
+		public int GetDurationFrames(float overrideFrameRate = 0f)
+		{
+			int result = 0;
+			float frameRate = (overrideFrameRate > 0f)?overrideFrameRate:GetVideoFrameRate();
+			if (frameRate > 0f)
+			{
+				result = Helper.ConvertTimeSecondsToFrame(GetDuration(), frameRate);
+			}
+			return result;
+		}
+
+		/// <inheritdoc/>
+		public int GetMaxFrameNumber(float overrideFrameRate = 0f)
+		{
+			int result = GetDurationFrames();
+			result = Mathf.Max(0, result - 1);
+			return result;
+		}
+
+		/// <inheritdoc/>
+		public void SeekToFrameRelative(int frameOffset, float overrideFrameRate = 0f)
+		{
+			float frameRate = (overrideFrameRate > 0f)?overrideFrameRate:GetVideoFrameRate();
+			if (frameRate > 0f)
+			{
+				int frame = Helper.ConvertTimeSecondsToFrame(GetCurrentTime(), frameRate);
+				frame += frameOffset;
+				frame = Mathf.Clamp(frame, 0, GetMaxFrameNumber(frameRate));
+				double time = Helper.ConvertFrameToTimeSeconds(frame, frameRate);
+				Seek(time);
+			}
+		}
+
+		/// <inheritdoc/>
+		public void SeekToFrame(int frame, float overrideFrameRate = 0f)
+		{
+			float frameRate = (overrideFrameRate > 0f)?overrideFrameRate:GetVideoFrameRate();
+			if (frameRate > 0f)
+			{
+				frame = Mathf.Clamp(frame, 0, GetMaxFrameNumber(frameRate));
+				double time = Helper.ConvertFrameToTimeSeconds(frame, frameRate);
+				Seek(time);
+			}
+		}
+
+		#region IBufferedDisplay Implementation
+
+		private int _unityFrameCountBufferedDisplayGuard = -1;
+
+		/// <inheritdoc/>
+		public long UpdateBufferedDisplay()
+		{
+			// Guard to make sure we're only updating the buffered frame once per Unity frame
+			if (Time.frameCount == _unityFrameCountBufferedDisplayGuard) return GetTextureTimeStamp();
+
+			_unityFrameCountBufferedDisplayGuard = Time.frameCount;
+
+			return InternalUpdateBufferedDisplay();
+		}
+
+		internal virtual long InternalUpdateBufferedDisplay() { return 0; }
+
+		/// <inheritdoc/>
+		public virtual BufferedFramesState GetBufferedFramesState()
+		{
+			return new BufferedFramesState();
+		}
+
+		/// <inheritdoc/>
+		public virtual void SetSlaves(IBufferedDisplay[] slaves) { }
+
+		/// <inheritdoc/>
+		public virtual void SetBufferedDisplayMode(BufferedFrameSelectionMode mode, IBufferedDisplay master = null) { }
+
+		/// <inheritdoc/>
+		public virtual void SetBufferedDisplayOptions(bool pauseOnPrerollComplete) { }
+
+		#endregion // IBufferedDisplay Implementation
+
+		protected PlaybackQualityStats _playbackQualityStats = new PlaybackQualityStats();
+
+		public PlaybackQualityStats GetPlaybackQualityStats()
+		{
+			return _playbackQualityStats;
+		}
+	}
+}

+ 12 - 0
Assets/AVProVideo/Runtime/Scripts/Internal/BaseMediaPlayer.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 4f59504ca098e7d41b036917f4764ee0
+timeCreated: 1447782861
+licenseType: Free
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 106 - 0
Assets/AVProVideo/Runtime/Scripts/Internal/Events.cs

@@ -0,0 +1,106 @@
+using UnityEngine.Events;
+using System.Collections.Generic;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	[System.Serializable]
+	public class MediaPlayerLoadEvent : UnityEvent<string> {}
+
+	[System.Serializable]
+	public class MediaPlayerEvent : UnityEvent<MediaPlayer, MediaPlayerEvent.EventType, ErrorCode>
+	{
+		public enum EventType
+		{
+			MetaDataReady,		// Triggered when meta data(width, duration etc) is available
+			ReadyToPlay,		// Triggered when the video is loaded and ready to play
+			Started,			// Triggered when the playback starts
+			FirstFrameReady,	// Triggered when the first frame has been rendered
+			FinishedPlaying,	// Triggered when a non-looping video has finished playing
+			Closing,			// Triggered when the media is closed
+			Error,				// Triggered when an error occurs
+			SubtitleChange,		// Triggered when the subtitles change
+			Stalled,			// Triggered when media is stalled (eg. when lost connection to media stream)
+			Unstalled,			// Triggered when media is resumed form a stalled state (eg. when lost connection is re-established)
+			ResolutionChanged,	// Triggered when the resolution of the video has changed (including the load) Useful for adaptive streams
+			StartedSeeking,		// Triggered when seeking begins
+			FinishedSeeking,    // Triggered when seeking has finished
+			StartedBuffering,	// Triggered when buffering begins
+			FinishedBuffering,	// Triggered when buffering has finished
+			PropertiesChanged,	// Triggered when any properties (eg stereo packing are changed) - this has to be triggered manually
+			PlaylistItemChanged,// Triggered when the new item is played in the playlist
+			PlaylistFinished,	// Triggered when the playlist reaches the end
+
+			TextTracksChanged,	// Triggered when the text tracks are added or removed
+			TextCueChanged = SubtitleChange,	// Triggered when the text to display changes
+
+			// TODO: 
+			//StartLoop,		// Triggered when the video starts and is in loop mode
+			//EndLoop,			// Triggered when the video ends and is in loop mode
+			//NewFrame			// Trigger when a new video frame is available
+		}
+
+		private List<UnityAction<MediaPlayer, MediaPlayerEvent.EventType, ErrorCode>> _listeners = new List<UnityAction<MediaPlayer, EventType, ErrorCode>>(4);
+
+		public bool HasListeners()
+		{
+			return (_listeners.Count > 0) || (GetPersistentEventCount() > 0);
+		}
+
+		new public void AddListener(UnityAction<MediaPlayer, MediaPlayerEvent.EventType, ErrorCode> call)
+		{
+			if (!_listeners.Contains(call))
+			{
+				_listeners.Add(call);
+				base.AddListener(call);
+			}
+		}
+
+		new public void RemoveListener(UnityAction<MediaPlayer, MediaPlayerEvent.EventType, ErrorCode> call)
+		{
+			int index = _listeners.IndexOf(call);
+			if (index >= 0)
+			{
+				_listeners.RemoveAt(index);
+				base.RemoveListener(call);
+			}
+		}
+
+		new public void RemoveAllListeners()
+		{
+			_listeners.Clear();
+			base.RemoveAllListeners();
+		}
+	}
+
+#if false
+	public interface IMediaEvents
+	{
+		void				AddEventListener(UnityAction<MediaPlayer, MediaPlayerEvent.EventType, ErrorCode> call);
+		void				RemoveListener(UnityAction<MediaPlayer, MediaPlayerEvent.EventType, ErrorCode> call);
+		void				RemoveAllEventListeners();
+	}
+
+	public partial class BaseMediaPlayer
+	{
+		void AddEventListener(UnityAction<MediaPlayer, MediaPlayerEvent.EventType, ErrorCode> call)
+		{
+
+		}
+		void RemoveListener(UnityAction<MediaPlayer, MediaPlayerEvent.EventType, ErrorCode> call)
+		{
+
+		}
+		void RemoveAllEventListeners()
+		{
+
+		}
+
+		private MediaPlayerEvent _eventHandler;
+
+	}
+#endif
+}

+ 12 - 0
Assets/AVProVideo/Runtime/Scripts/Internal/Events.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 16a5efbe992a09144ac89dde2b3e0898
+timeCreated: 1438695622
+licenseType: Pro
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 519 - 0
Assets/AVProVideo/Runtime/Scripts/Internal/Helper.cs

@@ -0,0 +1,519 @@
+using UnityEngine;
+using System.Collections;
+using System.Collections.Generic;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	public static class Helper
+	{
+		public const string AVProVideoVersion = "2.7.3";
+		public sealed class ExpectedPluginVersion
+		{
+			public const string Windows      = "2.7.0";
+			public const string WinRT        = "2.7.0";
+			public const string Android      = "2.7.1";
+			public const string Apple        = "2.7.3";
+		}
+
+		public const string UnityBaseTextureName = "_MainTex";
+		public const string UnityBaseTextureName_URP = "_BaseMap";
+		public const string UnityBaseTextureName_HDRP = "_BaseColorMap";
+
+		public static string GetPath(MediaPathType location)
+		{
+			string result = string.Empty;
+			switch (location)
+			{
+				case MediaPathType.AbsolutePathOrURL:
+					break;
+				case MediaPathType.RelativeToDataFolder:
+					result = Application.dataPath;
+					break;
+				case MediaPathType.RelativeToPersistentDataFolder:
+					result = Application.persistentDataPath;
+					break;
+				case MediaPathType.RelativeToProjectFolder:
+#if !UNITY_WINRT_8_1
+					string path = "..";
+#if UNITY_STANDALONE_OSX && !UNITY_EDITOR_OSX
+						path += "/..";
+#endif
+					result = System.IO.Path.GetFullPath(System.IO.Path.Combine(Application.dataPath, path));
+					result = result.Replace('\\', '/');
+#endif
+					break;
+				case MediaPathType.RelativeToStreamingAssetsFolder:
+					result = Application.streamingAssetsPath;
+					break;
+			}
+			return result;
+		}
+
+		public static string GetFilePath(string path, MediaPathType location)
+		{
+			string result = string.Empty;
+			if (!string.IsNullOrEmpty(path))
+			{
+				switch (location)
+				{
+					case MediaPathType.AbsolutePathOrURL:
+						result = path;
+						break;
+					case MediaPathType.RelativeToDataFolder:
+					case MediaPathType.RelativeToPersistentDataFolder:
+					case MediaPathType.RelativeToProjectFolder:
+					case MediaPathType.RelativeToStreamingAssetsFolder:
+						result = System.IO.Path.Combine(GetPath(location), path);
+						break;
+				}
+			}
+			return result;
+		}
+
+		public static string GetFriendlyResolutionName(int width, int height, float fps)
+		{
+			// List of common 16:9 resolutions
+			int[] areas = { 0, 7680 * 4320, 3840 * 2160, 2560 * 1440, 1920 * 1080, 1280 * 720, 853 * 480, 640 * 360, 426 * 240, 256 * 144 };
+			string[] names = { "Unknown", "8K", "4K", "1440p", "1080p", "720p", "480p", "360p", "240p", "144p" };
+
+			Debug.Assert(areas.Length == names.Length);
+
+			// Find the closest resolution
+			int closestAreaIndex = 0;
+			int area = width * height;
+			int minDelta = int.MaxValue;
+			for (int i = 0; i < areas.Length; i++)
+			{
+				int d = Mathf.Abs(areas[i] - area);
+				// TODO: add a maximum threshold to ignore differences that are too high
+				if (d < minDelta)
+				{
+					closestAreaIndex = i;
+					minDelta = d;
+					// If the exact mode is found, early out
+					if (d == 0)
+					{
+						break;
+					}
+				}
+			}
+
+			string result = names[closestAreaIndex];
+
+			// Append frame rate if valid
+			if (fps > 0f && !float.IsNaN(fps))
+			{
+				result += fps.ToString("0.##");
+			}
+
+			return result;
+		}
+
+		public static string GetErrorMessage(ErrorCode code)
+		{
+			string result = string.Empty;
+			switch (code)
+			{
+				case ErrorCode.None:
+					result = "No Error";
+					break;
+				case ErrorCode.LoadFailed:
+					result = "Loading failed.  File not found, codec not supported, video resolution too high or insufficient system resources.";
+#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
+					// Add extra information for older Windows versions that don't have support for modern codecs
+					if (SystemInfo.operatingSystem.StartsWith("Windows XP") ||
+						SystemInfo.operatingSystem.StartsWith("Windows Vista"))
+					{
+						result += " NOTE: Windows XP and Vista don't have native support for H.264 codec.  Consider using an older codec such as DivX or installing 3rd party codecs such as LAV Filters.";
+					}
+#endif
+					break;
+				case ErrorCode.DecodeFailed:
+					result = "Decode failed.  Possible codec not supported, video resolution/bit-depth too high, or insufficient system resources.";
+#if UNITY_ANDROID
+					result += " On Android this is generally due to the hardware not having enough resources to decode the video. Most Android devices can only handle a maximum of one 4K video at once.";
+#endif
+					break;
+			}
+			return result;
+		}
+
+		public static string GetPlatformName(Platform platform)
+		{
+			string result = "Unknown";
+			switch (platform)
+			{
+				case Platform.WindowsUWP:
+					result = "Windows UWP";
+					break;
+				case Platform.MacOSX:
+					result = "macOS";
+					break;
+				default:
+					result = platform.ToString();
+				break;
+			}
+			return result;
+		}
+
+		public static string[] GetPlatformNames()
+		{
+			return new string[] {
+				GetPlatformName(Platform.Windows),
+				GetPlatformName(Platform.MacOSX),
+				GetPlatformName(Platform.iOS),
+				GetPlatformName(Platform.tvOS),
+				GetPlatformName(Platform.Android),
+				GetPlatformName(Platform.WindowsUWP),
+				GetPlatformName(Platform.WebGL),
+			};
+		}
+
+#if AVPROVIDEO_DISABLE_LOGGING
+		[System.Diagnostics.Conditional("ALWAYS_FALSE")]
+#endif
+		public static void LogInfo(string message, Object context = null)
+		{
+			if (context == null)
+			{
+				Debug.Log("[AVProVideo] " + message);
+			}
+			else
+			{
+				Debug.Log("[AVProVideo] " + message, context);
+			}
+		}
+
+		public static int GetUnityAudioSampleRate()
+		{
+			// For standalone builds (not in the editor):
+			// In Unity 4.6, 5.0, 5.1 when audio is disabled there is no indication from the API.
+			// But in 5.2.0 and above, it logs an error when trying to call
+			// AudioSettings.GetDSPBufferSize() or AudioSettings.outputSampleRate
+			// So to prevent the error, check if AudioSettings.GetConfiguration().sampleRate == 0
+			return (AudioSettings.GetConfiguration().sampleRate == 0) ? 0 : AudioSettings.outputSampleRate;
+		}
+
+		public static int GetUnityAudioSpeakerCount()
+		{
+			switch (AudioSettings.GetConfiguration().speakerMode)
+			{
+				case AudioSpeakerMode.Mono: return 1;
+				case AudioSpeakerMode.Stereo: return 2;
+				case AudioSpeakerMode.Quad: return 4;
+				case AudioSpeakerMode.Surround: return 5;
+				case AudioSpeakerMode.Mode5point1: return 6;
+				case AudioSpeakerMode.Mode7point1: return 8;
+				case AudioSpeakerMode.Prologic: return 2;
+			}
+			return 0;
+		}
+
+		// Returns a valid range to use for a timeline display
+		// Either it will return the range 0..duration, or
+		// for live streams it will return first seekable..last seekable time
+		public static TimeRange GetTimelineRange(double duration, TimeRanges seekable)
+		{
+			TimeRange result = new TimeRange();
+			if (duration >= 0.0 && duration < 2e10)
+			{
+				// Duration is valid
+				result.startTime = 0f;
+				result.duration = duration;
+			}
+			else
+			{
+				// Duration is invalid, so it could be a live stream, so derive from seekable range
+				result.startTime = seekable.MinTime;
+				result.duration = seekable.Duration;
+			}
+			return result;
+		}
+
+		public const double SecondsToHNS = 10000000.0;
+		public const double MilliSecondsToHNS = 10000.0;
+
+		public static string GetTimeString(double timeSeconds, bool showMilliseconds = false)
+		{
+			float totalSeconds = (float)timeSeconds;
+			int hours = Mathf.FloorToInt(totalSeconds / (60f * 60f));
+			float usedSeconds = hours * 60f * 60f;
+
+			int minutes = Mathf.FloorToInt((totalSeconds - usedSeconds) / 60f);
+			usedSeconds += minutes * 60f;
+
+			int seconds = Mathf.FloorToInt(totalSeconds - usedSeconds);
+
+			string result;
+			if (hours <= 0)
+			{
+				if (showMilliseconds)
+				{
+					int milliSeconds = (int)((totalSeconds - Mathf.Floor(totalSeconds)) * 1000f);
+					result = string.Format("{0:00}:{1:00}:{2:000}", minutes, seconds, milliSeconds);
+				}
+				else
+				{
+					result = string.Format("{0:00}:{1:00}", minutes, seconds);
+				}
+			}
+			else
+			{
+				if (showMilliseconds)
+				{
+					int milliSeconds = (int)((totalSeconds - Mathf.Floor(totalSeconds)) * 1000f);
+					result = string.Format("{2}:{0:00}:{1:00}:{3:000}", minutes, seconds, hours, milliSeconds);
+				}
+				else
+				{
+					result = string.Format("{2}:{0:00}:{1:00}", minutes, seconds, hours);
+				}
+			}
+
+			return result;
+		}
+
+		/// <summary>
+		/// Convert texture transform matrix to an enum of orientation types
+		/// </summary>
+		public static Orientation GetOrientation(float[] t)
+		{
+			Orientation result = Orientation.Landscape;
+			if (t != null)
+			{
+				// TODO: check that the Portrait and PortraitFlipped are the right way around
+				if (t[0] == 0f && t[1]== 1f && t[2] == -1f && t[3] == 0f)
+				{
+					result = Orientation.Portrait;
+				} else
+				if (t[0] == 0f && t[1] == -1f && t[2] == 1f && t[3] == 0f)
+				{
+					result = Orientation.PortraitFlipped;
+				} else
+				if (t[0]== 1f && t[1] == 0f && t[2] == 0f && t[3] == 1f)
+				{
+					result = Orientation.Landscape;
+				} else
+				if (t[0] == -1f && t[1] == 0f && t[2] == 0f && t[3] == -1f)
+				{
+					result = Orientation.LandscapeFlipped;
+				}
+				else
+				if (t[0] == 0f && t[1] == 1f && t[2] == 1f && t[3] == 0f)
+				{
+					result = Orientation.PortraitHorizontalMirror;
+				}
+			}
+			return result;
+		}
+
+		private static Matrix4x4 PortraitMatrix         = Matrix4x4.TRS(new Vector3(0f, 1f, 0f), Quaternion.Euler(0f, 0f, -90f), Vector3.one);
+		private static Matrix4x4 PortraitFlippedMatrix  = Matrix4x4.TRS(new Vector3(1f, 0f, 0f), Quaternion.Euler(0f, 0f, 90f), Vector3.one);
+		private static Matrix4x4 LandscapeFlippedMatrix = Matrix4x4.TRS(new Vector3(0f, 1f, 0f), Quaternion.Euler(0f, 0f, -90f), Vector3.one);
+
+		public static Matrix4x4 GetMatrixForOrientation(Orientation ori)
+		{
+			Matrix4x4 result;
+			switch (ori)
+			{
+				case Orientation.Landscape:
+					result = Matrix4x4.identity;
+					break;
+				case Orientation.LandscapeFlipped:
+					result = LandscapeFlippedMatrix;
+					break;
+				case Orientation.Portrait:
+					result = PortraitMatrix;
+					break;
+				case Orientation.PortraitFlipped:
+					result = PortraitFlippedMatrix;
+					break;
+				case Orientation.PortraitHorizontalMirror:
+					result = new Matrix4x4();
+					result.SetColumn(0, new Vector4(0f, 1f, 0f, 0f));
+					result.SetColumn(1, new Vector4(1f, 0f, 0f, 0f));
+					result.SetColumn(2, new Vector4(0f, 0f, 1f, 0f));
+					result.SetColumn(3, new Vector4(0f, 0f, 0f, 1f));
+					break;
+				default:
+					throw new System.Exception("Unknown Orientation type");
+			}
+			return result;
+		}
+
+		public static int ConvertTimeSecondsToFrame(double seconds, float frameRate)
+		{
+			// NOTE: Generally you should use RountToInt when converting from time to frame number
+			// but because we're adding a half frame offset (which seems to be the safer thing to do) we need to FloorToInt
+			seconds = System.Math.Max(0.0, seconds);
+			frameRate = Mathf.Max(0f, frameRate);
+			return (int)System.Math.Floor(frameRate * seconds);
+		}
+
+		public static double ConvertFrameToTimeSeconds(int frame, float frameRate)
+		{
+			frame = Mathf.Max(0, frame);
+			frameRate = Mathf.Max(0f, frameRate);
+			double frameDurationSeconds = 1.0 / frameRate;
+			return ((double)frame * frameDurationSeconds) + (frameDurationSeconds * 0.5);		// Add half a frame we that the time lands in the middle of the frame range and not at the edges
+		}
+
+		public static double FindNextKeyFrameTimeSeconds(double seconds, float frameRate, int keyFrameInterval)
+		{
+			seconds = System.Math.Max(0.0, seconds);
+			frameRate = Mathf.Max(0f, frameRate);
+			keyFrameInterval = Mathf.Max(0, keyFrameInterval);
+			int currentFrame = Helper.ConvertTimeSecondsToFrame(seconds, frameRate);
+			// TODO: allow specifying a minimum number of frames so that if currentFrame is too close to nextKeyFrame, it will calculate the next-next keyframe
+			int nextKeyFrame = keyFrameInterval * Mathf.CeilToInt((float)(currentFrame + 1) / (float)keyFrameInterval);
+			return Helper.ConvertFrameToTimeSeconds(nextKeyFrame, frameRate);
+		}
+
+		public static System.DateTime ConvertSecondsSince1970ToDateTime(double secondsSince1970)
+		{
+			System.TimeSpan time = System.TimeSpan.FromSeconds(secondsSince1970);
+			return new System.DateTime(1970, 1, 1).Add(time);
+		}
+
+	#if (UNITY_EDITOR_WIN || (!UNITY_EDITOR && UNITY_STANDALONE_WIN))
+		[System.Runtime.InteropServices.DllImport("kernel32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode, EntryPoint = "GetShortPathNameW", SetLastError=true)]
+		private static extern int GetShortPathName([System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPWStr)] string pathName,
+													[System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPWStr)] System.Text.StringBuilder shortName,
+													int cbShortName);
+
+		// Handle very long file paths by converting to DOS 8.3 format
+		internal static string ConvertLongPathToShortDOS83Path(string path)
+		{
+			const string pathToken = @"\\?\";
+			string result = pathToken + path.Replace("/","\\");
+			int length = GetShortPathName(result, null, 0);
+			if (length > 0)
+			{
+				System.Text.StringBuilder sb = new System.Text.StringBuilder(length);
+				if (0 != GetShortPathName(result, sb, length))
+				{
+					result = sb.ToString().Replace(pathToken, "");
+					Debug.LogWarning("[AVProVideo] Long path detected. Changing to DOS 8.3 format");
+				}
+			}
+			return result;
+		}
+	#endif
+
+		// Converts a non-readable texture to a readable Texture2D.
+		// "targetTexture" can be null or you can pass in an existing texture.
+		// Remember to Destroy() the returned texture after finished with it
+		public static Texture2D GetReadableTexture(Texture inputTexture, bool requiresVerticalFlip, Orientation ori, Texture2D targetTexture = null)
+		{
+			Texture2D resultTexture = targetTexture;
+
+			RenderTexture prevRT = RenderTexture.active;
+
+			int textureWidth = inputTexture.width;
+			int textureHeight = inputTexture.height;
+#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE || UNITY_IOS || UNITY_TVOS
+			if (ori == Orientation.Portrait || ori == Orientation.PortraitFlipped)
+			{
+				textureWidth = inputTexture.height;
+				textureHeight = inputTexture.width;
+			}
+#endif
+
+			// Blit the texture to a temporary RenderTexture
+			// This handles any format conversion that is required and allows us to use ReadPixels to copy texture from RT to readable texture
+			RenderTexture tempRT = RenderTexture.GetTemporary(textureWidth, textureHeight, 0, RenderTextureFormat.ARGB32);
+
+			if (ori == Orientation.Landscape)
+			{
+				if (!requiresVerticalFlip)
+				{
+					Graphics.Blit(inputTexture, tempRT);
+				}
+				else
+				{
+					// The above Blit can't flip unless using a material, so we use Graphics.DrawTexture instead
+					GL.PushMatrix();
+					RenderTexture.active = tempRT;
+					GL.LoadPixelMatrix(0f, tempRT.width, 0f, tempRT.height);
+					Rect sourceRect = new Rect(0f, 0f, 1f, 1f);
+					// NOTE: not sure why we need to set y to -1, without this there is a 1px gap at the bottom
+					Rect destRect = new Rect(0f, -1f, tempRT.width, tempRT.height);
+
+					Graphics.DrawTexture(destRect, inputTexture, sourceRect, 0, 0, 0, 0);
+					GL.PopMatrix();
+					GL.InvalidateState();
+				}
+			}
+#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE || UNITY_IOS || UNITY_TVOS
+			else
+			{
+				Matrix4x4 m = Matrix4x4.identity;
+				switch (ori)
+				{
+					case Orientation.Portrait:
+						m = Matrix4x4.TRS(new Vector3(0f, inputTexture.width, 0f), Quaternion.Euler(0f, 0f, -90f), Vector3.one);
+						break;
+					case Orientation.PortraitFlipped:
+						m = Matrix4x4.TRS(new Vector3(inputTexture.height, 0f, 0f), Quaternion.Euler(0f, 0f, 90f), Vector3.one);
+						break;
+					case Orientation.LandscapeFlipped:
+						m = Matrix4x4.TRS(new Vector3(inputTexture.width, inputTexture.height, 0f), Quaternion.identity, new Vector3(-1f, -1f, 1f));
+						break;
+				}
+
+				// The above Blit can't flip unless using a material, so we use Graphics.DrawTexture instead
+				GL.InvalidateState();
+				GL.PushMatrix();
+				GL.Clear(false, true, Color.red);
+				RenderTexture.active = tempRT;
+				GL.LoadPixelMatrix(0f, tempRT.width, 0f, tempRT.height);
+				Rect sourceRect = new Rect(0f, 0f, 1f, 1f);
+				// NOTE: not sure why we need to set y to -1, without this there is a 1px gap at the bottom
+				Rect destRect = new Rect(0f, -1f, inputTexture.width, inputTexture.height);
+				GL.MultMatrix(m);
+
+				Graphics.DrawTexture(destRect, inputTexture, sourceRect, 0, 0, 0, 0);
+				GL.PopMatrix();
+				GL.InvalidateState();
+			}
+#endif
+
+			if (resultTexture == null)
+			{
+				resultTexture = new Texture2D(textureWidth, textureHeight, TextureFormat.ARGB32, false);
+			}
+
+			RenderTexture.active = tempRT;
+			resultTexture.ReadPixels(new Rect(0f, 0f, textureWidth, textureHeight), 0, 0, false);
+			resultTexture.Apply(false, false);
+			RenderTexture.ReleaseTemporary(tempRT);
+
+			RenderTexture.active = prevRT;
+
+			return resultTexture;
+		}
+
+		// Converts a non-readable texture to a readable Texture2D.
+		// "targetTexture" can be null or you can pass in an existing texture.
+		// Remember to Destroy() the returned texture after finished with it
+		public static Texture2D GetReadableTexture(RenderTexture inputTexture, Texture2D targetTexture = null)
+		{
+			if (targetTexture == null)
+			{
+				targetTexture = new Texture2D(inputTexture.width, inputTexture.height, TextureFormat.ARGB32, false);
+			}
+
+			RenderTexture prevRT = RenderTexture.active;
+			RenderTexture.active = inputTexture;
+			targetTexture.ReadPixels(new Rect(0f, 0f, inputTexture.width, inputTexture.height), 0, 0, false);
+			targetTexture.Apply(false, false);
+			RenderTexture.active = prevRT;
+
+			return targetTexture;
+		}
+	}
+}

+ 12 - 0
Assets/AVProVideo/Runtime/Scripts/Internal/Helper.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 79e446998599e1647804321292c80f42
+timeCreated: 1600887818
+licenseType: Pro
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 982 - 0
Assets/AVProVideo/Runtime/Scripts/Internal/Interfaces.cs

@@ -0,0 +1,982 @@
+using UnityEngine;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	public interface IMediaPlayer
+	{
+		void OnEnable();
+		void Update();
+		void EndUpdate();
+		void Render();
+		IntPtr GetNativePlayerHandle();
+	}
+
+	/// <summary>
+	/// Interface for side loading of subtitles in SRT format
+	/// </summary>
+	public interface IMediaSubtitles
+	{
+		bool LoadSubtitlesSRT(string data);
+		int GetSubtitleIndex();
+		string GetSubtitleText();
+	}
+
+	public enum BufferedFrameSelectionMode : int
+	{
+		// No buffering, just selects the latest decoded frame
+		None = 0,
+
+		// Selects the newest buffered frame, and displays it until a newer frame is available
+		NewestFrame = 10,
+
+		// Selects the oldest buffered frame, and displays it until a newer frame is available
+		OldestFrame = 11,
+
+		// Selects the next buffered frame, and displays it until the number of buffered frames changes
+		MediaClock = 20,
+
+		// Uses Time.deltaTime to keep a clock which is used to select the buffered frame
+		ElapsedTime = 30,
+
+		// Uses VSync delta time to keep a clock which is used to select the buffered frame
+		// Time.deltaTime is used to calculate the number of vsyncs that have elapsed
+		ElapsedTimeVsynced = 40,
+
+		// Selects the buffered frame corresponding to the external timeStamp (useful for frame-syncing players)
+		FromExternalTime = 50,
+
+		// Selects the closest buffered frame corresponding to the external timeStamp (useful for frame-syncing players)
+		FromExternalTimeClosest = 51,
+	}
+
+	/// <summary>
+	/// Interface for buffering frames for more control over the timing of their display
+	/// </summary>
+	public interface IBufferedDisplay
+	{
+		/// <summary>
+		/// We need to manually call UpdateBufferedDisplay() in the case of master-slave synced playback so that master is updated before slaves
+		/// </summary>
+		long						UpdateBufferedDisplay();
+
+		BufferedFramesState			GetBufferedFramesState();
+
+		void						SetSlaves(IBufferedDisplay[] slaves);
+
+		void						SetBufferedDisplayMode(BufferedFrameSelectionMode mode, IBufferedDisplay master = null);
+
+		void						SetBufferedDisplayOptions(bool pauseOnPrerollComplete);
+	}
+
+	public interface IMediaControl
+	{
+		/// <summary>
+		/// Be careful using this method directly.  It is best to instead use the OpenMedia() method in the MediaPlayer component as this will set up the events correctly and also perform other checks
+		/// customHttpHeaders is in the format "key1:value1\r\nkey2:value2\r\n"=
+		/// </summary>
+		bool	OpenMedia(string path, long offset, string customHttpHeaders, MediaHints mediahints, int forceFileFormat = 0, bool startWithHighestBitrate = false);
+		bool	OpenMediaFromBuffer(byte[] buffer);
+		bool	StartOpenMediaFromBuffer(ulong length);
+		bool	AddChunkToMediaBuffer(byte[] chunk, ulong offset, ulong length);
+		bool	EndOpenMediaFromBuffer();
+
+#if NETFX_CORE
+		bool	OpenMedia(Windows.Storage.Streams.IRandomAccessStream ras, string path, long offset, string customHttpHeaders);
+#endif
+
+		void	CloseMedia();
+
+		void	SetLooping(bool bLooping);
+		bool	IsLooping();
+
+		bool	HasMetaData();
+		bool	CanPlay();
+		bool	IsPlaying();
+		bool	IsSeeking();
+		bool	IsPaused();
+		bool	IsFinished();
+		bool	IsBuffering();
+
+		void	Play();
+		void	Pause();
+		void	Stop();
+		void	Rewind();
+
+		/// <summary>
+		/// The time in seconds seeked will be to the exact time
+		/// This can take a long time is the keyframes are far apart
+		/// Some platforms don't support this and instead seek to the closest keyframe
+		/// </summary>
+		void	Seek(double time);
+
+		/// <summary>
+		/// The time in seconds seeked will be to the closest keyframe
+		/// </summary>
+		void	SeekFast(double time);
+
+		/// <summary>
+		/// The time in seconds seeked to will be within the range [time-timeDeltaBefore, time+timeDeltaAfter] for efficiency.
+		/// Only supported on macOS, iOS and tvOS.
+		/// Other platforms will automatically pass through to Seek()
+		/// </summary>
+		void	SeekWithTolerance(double time, double timeDeltaBefore, double timeDeltaAfter);
+
+		/// <summary>
+		/// Seek to a specific frame, range is [0, GetMaxFrameNumber()]
+		/// NOTE: For best results the video should be encoded as keyframes only
+		/// and have no audio track, or an audio track with the same length as the video track
+		/// </summary>
+		void	SeekToFrame(int frame, float overrideFrameRate = 0f);
+
+		/// <summary>
+		/// Seek forwards or backwards relative to the current frame
+		/// NOTE: For best results the video should be encoded as keyframes only
+		/// and have no audio track, or an audio track with the same length as the video track
+		/// </summary>
+		void	SeekToFrameRelative(int frameOffset, float overrideFrameRate = 0f);
+
+		/// <summary>
+		/// Returns the current video time in seconds
+		/// </summary>
+		double	GetCurrentTime();
+
+		/// <summary>
+		/// Returns the current video time in frames, range is [0, GetMaxFrameNumber()]
+		/// NOTE: For best results the video should be encoded as keyframes only
+		/// and have no audio track, or an audio track with the same length as the video track
+		/// </summary>
+		int		GetCurrentTimeFrames(float overrideFrameRate = 0f);
+
+		/// <summary>
+		/// Returns the current video date and time usually from the
+		/// EXT-X-PROGRAM-DATE-TIME tag on HLS streams
+		/// Only supported on macOS, iOS, tvOS and Android (using ExoPlayer API)
+		/// And Windows 10 using WinRT API
+		/// </summary>
+		System.DateTime		GetProgramDateTime();
+
+		float	GetPlaybackRate();
+		void	SetPlaybackRate(float rate);
+
+		void	MuteAudio(bool bMute);
+		bool	IsMuted();
+		void	SetVolume(float volume);
+		void	SetBalance(float balance);
+		float	GetVolume();
+		float	GetBalance();
+
+		/*int		GetCurrentVideoTrack();
+		void	SetVideoTrack(int index);
+
+		int		GetCurrentAudioTrack();
+		void	SetAudioTrack(int index);*/
+
+		/// <summary>
+		/// Returns a range of time values that can be seeked in seconds
+		/// </summary>
+		TimeRanges 					GetSeekableTimes();
+
+		/// <summary>
+		/// Returns a range of time values that contain fully downloaded segments,
+		/// which can be seeked to immediately without requiring additional downloading
+		/// </summary>
+		TimeRanges 					GetBufferedTimes();
+
+		ErrorCode 					GetLastError();
+		long						GetLastExtendedErrorCode();
+
+		void						SetTextureProperties(FilterMode filterMode = FilterMode.Bilinear, TextureWrapMode wrapMode = TextureWrapMode.Clamp, int anisoLevel = 1);
+		void						GetTextureProperties(out FilterMode filterMode, out TextureWrapMode wrapMode, out int anisoLevel);
+
+		// Audio Grabbing
+
+		/// <summary>
+		/// Copies the specified amount of audio into the buffer
+		/// If the specified amount is not yet available then nothing no samples are copied
+		/// The number of audio samples grabbed are returned
+		/// </summary>
+		int							GrabAudio(float[] buffer, int sampleCount, int channelCount);
+		int							GetAudioBufferedSampleCount();
+		int							GetAudioChannelCount();
+		AudioChannelMaskFlags		GetAudioChannelMask();
+
+		void AudioConfigurationChanged(bool deviceChanged);
+
+		// Audio 360
+
+		void	SetAudioChannelMode(Audio360ChannelMode channelMode);
+		void	SetAudioHeadRotation(Quaternion q);
+		void	ResetAudioHeadRotation();
+		void	SetAudioFocusEnabled(bool enabled);
+		void	SetAudioFocusProperties(float offFocusLevel, float widthDegrees);
+		void	SetAudioFocusRotation(Quaternion q);
+		void	ResetAudioFocus();
+
+		bool	WaitForNextFrame(Camera dummyCamera, int previousFrameCount);
+
+		[Obsolete("SetPlayWithoutBuffering has been deprecated, see platform specific options for how to enable playback without buffering (if supported).")]
+		void	SetPlayWithoutBuffering(bool playWithoutBuffering);
+
+		// Encrypted stream support
+		//void	SetKeyServerURL(string url);
+		void	SetKeyServerAuthToken(string token);
+		void	SetOverrideDecryptionKey(byte[] key);
+
+		// External playback support.
+
+		/// <summary>
+		/// Check to see if external playback is currently active on the player.
+		/// </summary>
+		bool IsExternalPlaybackActive();
+
+		/// <summary>
+		/// Set whether the player is allowed to switch to external playback, e.g. AirPlay.
+		/// </summary>
+		void SetAllowsExternalPlayback(bool enable);
+
+		/// <summary>
+		/// Sets the video gravity of the player for external playback only.
+		/// </summary>
+		void SetExternalPlaybackVideoGravity(ExternalPlaybackVideoGravity gravity);
+	}
+
+	public interface IMediaInfo
+	{
+		/// <summary>
+		/// Returns media duration in seconds
+		/// </summary>
+		double	GetDuration();
+
+		/// <summary>
+		/// Returns media duration in frames
+		/// NOTE: For best results the video should be encoded as keyframes only
+		/// and have no audio track, or an audio track with the same length as the video track
+		/// </summary>
+		int		GetDurationFrames(float overrideFrameRate = 0f);
+
+		/// <summary>
+		/// Returns highest frame number that can be seeked to
+		/// NOTE: For best results the video should be encoded as keyframes only
+		/// and have no audio track, or an audio track with the same length as the video track
+		/// </summary>
+		int		GetMaxFrameNumber(float overrideFrameRate = 0f);
+
+		/// <summary>
+		/// Returns video width in pixels
+		/// </summary>
+		int		GetVideoWidth();
+
+		/// <summary>
+		/// Returns video height in pixels
+		/// </summary>
+		int		GetVideoHeight();
+
+		/// <summary>
+		/// Returns the frame rate of the media.
+		/// </summary>
+		float	GetVideoFrameRate();
+
+		/// <summary>
+		/// Returns the current achieved display rate in frames per second
+		/// </summary>
+		float	GetVideoDisplayRate();
+
+		/// <summary>
+		/// Returns true if the media has a visual track
+		/// </summary>
+		bool	HasVideo();
+
+		/// <summary>
+		/// Returns true if the media has a audio track
+		/// </summary>
+		bool	HasAudio();
+
+		/// <summary>
+		/// Returns the a description of which playback path is used internally.
+		/// This can for example expose whether CPU or GPU decoding is being performed
+		/// For Windows the available player descriptions are:
+		///		"DirectShow" - legacy Microsoft API but still very useful especially with modern filters such as LAV
+		///		"MF-MediaEngine-Software" - uses the Windows 8.1 features of the Microsoft Media Foundation API, but software decoding
+		///		"MF-MediaEngine-Hardware" - uses the Windows 8.1 features of the Microsoft Media Foundation API, but GPU decoding
+		///	Android has "MediaPlayer" and "ExoPlayer"
+		///	macOS / tvOS / iOS just has "AVFoundation"
+		/// </summary>
+		string GetPlayerDescription();
+
+#if !AVPRO_NEW_GAMMA
+		/// <summary>
+		/// Whether this MediaPlayer instance supports linear color space
+		/// If it doesn't then a correction may have to be made in the shader
+		/// </summary>
+		bool PlayerSupportsLinearColorSpace();
+#endif
+
+		/// <summary>
+		/// Checks if the playback is in a stalled state
+		/// </summary>
+		bool IsPlaybackStalled();
+
+		/// <summary>
+		/// The affine transform of the texture as an array of six floats: a, b, c, d, tx, ty.
+		/// </summary>
+		float[] GetTextureTransform();
+
+		/// <summary>
+		/// Gets the estimated bandwidth used by all video players (in bits per second)
+		/// Currently only supported on Android when using ExoPlayer API
+		/// </summary>
+		long GetEstimatedTotalBandwidthUsed();
+
+		/*
+		string GetMediaDescription();
+		string GetVideoDescription();
+		string GetAudioDescription();*/
+
+		/// <summary>
+		/// Checks if the media is compatible with external playback, for instance via AirPlay.
+		/// </summary>
+		bool IsExternalPlaybackSupported();
+
+		// Internal method
+		bool GetDecoderPerformance(ref int activeDecodeThreadCount, ref int decodedFrameCount, ref int droppedFrameCount);
+
+		// Internal method
+		PlaybackQualityStats GetPlaybackQualityStats();
+	}
+
+	#region MediaCaching
+
+	/// <summary>Options for configuring media caching.</summary>
+	public class MediaCachingOptions
+	{
+		/// <summary>The minimum bitrate of the media to cache in bits per second.</summary>
+		public double  minimumRequiredBitRate;
+
+		/// <summary>The minimum resolution of the media to cache.</summary>
+		/// <remark>Only supported on Android and iOS 14 and later.</remark>
+		public Vector2 minimumRequiredResolution;
+
+		/// <summary>The maximum bitrate of the media to cache in bits per second.</summary>
+		/// <remark>Only supported on Android.</remark>
+		public double maximumRequiredBitRate;
+
+		/// <summary>The maximum resolution of the media to cache.</summary>
+		/// <remark>Only supported on Android.</remark>
+		public Vector2 maximumRequiredResolution;
+
+		/// <summary>Human readable title for the cached media.</summary>
+		/// <remark>iOS: This value will be displayed in the usage pane of the settings app.</remark>
+		public string title;
+
+		/// <summary>Optional artwork for the cached media in PNG format.</summary>
+		/// <remark>iOS: This value will be displayed in the usage pane of the settings app.</remark>
+		public byte[] artwork;
+	}
+
+	/// <summary>Status of the media item in the cache.</summary>
+	public enum CachedMediaStatus: int
+	{
+		/// <summary>The media has not been cached.</summary>
+		NotCached,
+		/// <summary>The media is being cached.</summary>
+		Caching,
+		/// <summary>The media is cached.</summary>
+		Cached,
+		/// <summary>The media is not cached, something went wrong - check the log.</summary>
+		Failed,
+		/// <summary>The media caching is paused.</summary>
+		Paused
+	}
+
+	/// <summary>Interface for the media cache.</summary>
+	public interface IMediaCache
+	{
+		/// <summary>Test to see if the player can cache media.</summary>
+		/// <returns>True if media caching is supported.</returns>
+		bool IsMediaCachingSupported();
+
+		/// <summary>Cache the media specified by url.</summary>
+		/// <param name="url">The url of the media.</param>
+		/// <param name="headers"></param>
+		/// <param name="options"></param>
+		void AddMediaToCache(string url, string headers = null, MediaCachingOptions options = null);
+
+		/// <summary>Cancels the download of the media specified by url.</summary>
+		/// <param name="url">The url of the media.</param>
+		void CancelDownloadOfMediaToCache(string url);
+
+		/// <summary>Pause the download of the media specified by url.</summary>
+		/// <param name="url">The url of the media.</param>
+		void PauseDownloadOfMediaToCache(string url);
+
+		/// <summary>Resume the download of the media specified by url.</summary>
+		/// <param name="url">The url of the media.</param>
+		void ResumeDownloadOfMediaToCache(string url);
+
+		/// <summary>Remove the cached media specified by url.</summary>
+		/// <param name="url">The url of the media.</param>
+		void RemoveMediaFromCache(string url);
+
+		/// <summary>Get the cached status for the media specified.</summary>
+		/// <param name="url">The url of the media.</param>
+		/// <param name="progress">The amount of the media that has been cached in the range [0...1].</param>
+		/// <returns>The status of the media.</returns>
+		CachedMediaStatus GetCachedMediaStatus(string url, ref float progress);
+
+//		/// <summary>Test if the currently open media is cached.</summary>
+//		/// <returns>True if the media is cached, false otherwise.</returns>
+//		bool IsMediaCached();
+	}
+
+	#endregion
+
+	public interface ITextureProducer
+	{
+		/// <summary>
+		/// Gets the number of textures produced by the media player.
+		/// </summary>
+		int GetTextureCount();
+
+		/// <summary>
+		/// Returns the Unity texture containing the current frame image.
+		/// The texture pointer will return null while the video is loading
+		/// This texture usually remains the same for the duration of the video.
+		/// There are cases when this texture can change, for instance: if the graphics device is recreated,
+		/// a new video is loaded, or if an adaptive stream (eg HLS) is used and it switches video streams.
+		/// </summary>
+		Texture GetTexture(int index = 0);
+
+		/// <summary>
+		/// Returns a count of how many times the texture has been updated
+		/// </summary>
+		int GetTextureFrameCount();
+
+		/// <summary>
+		/// Returns whether this platform supports counting the number of times the texture has been updated
+		/// </summary>
+		bool SupportsTextureFrameCount();
+
+		/// <summary>
+		/// Returns the presentation time stamp of the current texture
+		/// </summary>
+		long GetTextureTimeStamp();
+
+		/// <summary>
+		/// Returns the DAR/SAR ratio
+		/// </summary>
+		float GetTexturePixelAspectRatio();
+
+		/// <summary>
+		/// Returns true if the image on the texture is upside-down
+		/// </summary>
+		bool RequiresVerticalFlip();
+
+		/// <summary>
+		/// Returns the type of packing used for stereo content
+		/// </summary>
+		StereoPacking GetTextureStereoPacking();
+
+		/// <summary>
+		/// Returns the whether the texture has transparency
+		/// </summary>
+		TransparencyMode GetTextureTransparency();
+
+		/// <summary>
+		/// Returns the type of packing used for alpha content
+		/// </summary>
+		AlphaPacking GetTextureAlphaPacking();
+
+		/// <summary>
+		/// Returns the current transformation required to convert from YpCbCr to RGB colorspaces.
+		/// </summary>
+		Matrix4x4 GetYpCbCrTransform();
+
+#if AVPRO_NEW_GAMMA
+		/// <summary>
+		/// Returns the gamma type of a sampled pixel
+		/// Is the texture returns samples in linear gamma then no conversion is need when using Unity's linear color space mode
+		/// If it doesn't then a correction may have to be made in the shader
+		/// </summary>
+		TextureGamma GetTextureSampleGamma();
+
+		bool TextureRequiresGammaConversion();
+#endif
+	}
+
+	public enum Platform
+	{
+		Windows,
+		MacOSX,
+		iOS,
+		tvOS,
+		Android,
+		WindowsUWP,
+		WebGL,
+		Count = 7,
+		Unknown = 100,
+	}
+
+	public enum MediaSource
+	{
+		Reference,
+		Path,
+	}
+
+	public enum MediaPathType
+	{
+		AbsolutePathOrURL,
+		RelativeToProjectFolder,
+		RelativeToStreamingAssetsFolder,
+		RelativeToDataFolder,
+		RelativeToPersistentDataFolder,
+	}
+
+	[System.Serializable]
+	public class MediaPath
+	{
+		[SerializeField] MediaPathType _pathType = MediaPathType.RelativeToStreamingAssetsFolder;
+		public MediaPathType PathType { get { return _pathType; } internal set { _pathType = value; } }
+
+		[SerializeField] string _path = string.Empty;
+		public string Path { get { return _path; } internal set { _path = value; } }
+
+		public MediaPath()
+		{
+			_pathType = MediaPathType.RelativeToStreamingAssetsFolder;
+			_path = string.Empty;
+		}
+		public MediaPath(MediaPath copy)
+		{
+			_pathType = copy.PathType;
+			_path = copy.Path;
+		}
+		public MediaPath(string path, MediaPathType pathType)
+		{
+			_pathType = pathType;
+			_path = path;
+		}
+
+		public string GetResolvedFullPath()
+		{
+			string result = Helper.GetFilePath(_path, _pathType);
+
+			#if (UNITY_EDITOR_WIN || (!UNITY_EDITOR && UNITY_STANDALONE_WIN))
+			if (result.Length > 200 && !result.Contains("://"))
+			{
+				result = Helper.ConvertLongPathToShortDOS83Path(result);
+			}
+			#endif
+
+			return result;
+		}
+
+		public static bool operator == (MediaPath a, MediaPath b)
+		{
+			if ((object)a == null)
+				return (object)b == null;
+
+			return a.Equals(b);
+		}
+		public static bool operator != (MediaPath a, MediaPath b)
+		{
+			return !(a == b);
+		}
+
+		public override bool Equals(object obj)
+		{
+			if (obj == null || GetType() != obj.GetType())
+				return false;
+
+			var a = (MediaPath)obj;
+			return (_pathType == a._pathType && _path == a._path);
+		}
+
+		public override int GetHashCode()
+		{
+			return _pathType.GetHashCode() ^ _path.GetHashCode();
+		}
+	}
+
+	public enum OverrideMode
+	{
+		None,						// No overide, just use internal logic
+		Override,					// Manually override
+	}
+
+	public enum TextureGamma
+	{
+		SRGB,
+		Linear,
+
+		// Future HDR support
+		// PQ,
+		// HLG,
+	}
+
+	public enum StereoPacking : int
+	{
+		None = 0,					// Monoscopic
+		TopBottom = 1,				// Top is the left eye, bottom is the right eye
+		LeftRight = 2,				// Left is the left eye, right is the right eye
+		CustomUV = 3,				// Use the mesh UV to unpack, uv0=left eye, uv1=right eye
+		TwoTextures = 4,			// First texture left eye, second texture is right eye
+		Unknown = 10,
+	}
+
+	[System.Serializable]
+	public struct MediaHints
+	{
+		public TransparencyMode transparency;
+		public AlphaPacking alphaPacking;
+		public StereoPacking stereoPacking;
+
+		private static MediaHints defaultHints = new MediaHints();
+		public static MediaHints Default { get { return defaultHints; } }
+	}
+
+	[System.Serializable]
+	public struct VideoResolveOptions
+	{
+		[SerializeField] public bool applyHSBC;
+		[SerializeField, Range(0f, 1f)]	public float hue;
+		[SerializeField, Range(0f, 1f)]	public float saturation;
+		[SerializeField, Range(0f, 1f)]	public float brightness;
+		[SerializeField, Range(0f, 1f)]	public float contrast;
+		[SerializeField, Range(0.0001f, 10f)]	public float gamma;
+		[SerializeField] public Color tint;
+		[SerializeField] public bool generateMipmaps;
+
+		public bool IsColourAdjust()
+		{
+			return (applyHSBC && (hue != 0.0f || saturation != 0.5f || brightness != 0.5f || contrast != 0.5f || gamma != 1.0f));
+		}
+
+		internal void ResetColourAdjust()
+		{
+			hue = 0.0f;
+			saturation = 0.5f;
+			brightness = 0.5f;
+			contrast = 0.5f;
+			gamma = 1.0f;
+		}
+
+		public static VideoResolveOptions Create()
+		{
+			VideoResolveOptions result = new VideoResolveOptions()
+			{
+				tint = Color.white,
+			};
+			result.ResetColourAdjust();
+
+			return result;
+		}
+	}
+
+	/// Transparency Mode
+	public enum TransparencyMode
+	{
+		Opaque,
+		Transparent,
+	}
+
+	public enum StereoEye
+	{
+		Both,
+		Left,
+		Right,
+	}
+
+	public enum AlphaPacking
+	{
+		None,
+		TopBottom,
+		LeftRight,
+	}
+
+	public enum ErrorCode
+	{
+		None = 0,
+		LoadFailed = 100,
+		DecodeFailed = 200,
+	}
+
+	public enum Orientation
+	{
+		Landscape,				// Landscape Right (0 degrees)
+		LandscapeFlipped,		// Landscape Left (180 degrees)
+		Portrait,				// Portrait Up (90 degrees)
+		PortraitFlipped,        // Portrait Down (-90 degrees)
+		PortraitHorizontalMirror,	// Portrait that is mirrored horizontally
+	}
+
+	public enum VideoMapping
+	{
+		Unknown,
+		Normal,
+		EquiRectangular360,
+		EquiRectangular180,
+		CubeMap3x2,
+	}
+
+	public enum FileFormat
+	{
+		Unknown,
+		HLS,
+		DASH,
+		SmoothStreaming,
+	}
+
+	public static class Windows
+	{
+		public enum VideoApi
+		{
+			MediaFoundation,			// Windows 8.1 and above
+			DirectShow,					// Legacy API
+			WinRT,						// Windows 10 and above
+		};
+
+		public enum AudioOutput
+		{
+			System,						// Default
+			Unity,						// Media Foundation API only
+			FacebookAudio360,			// Media Foundation API only
+			None,						// Media Foundation API only
+		}
+
+		// WIP: Experimental feature to allow overriding audio device for VR headsets
+		public const string AudioDeviceOutputName_Vive = "HTC VIVE USB Audio";
+		public const string AudioDeviceOutputName_Rift = "Headphones (Rift Audio)";
+	}
+
+	public static class WindowsUWP
+	{
+		public enum VideoApi
+		{
+			MediaFoundation,			// UWP 8.1 and above
+			WinRT,						// UWP 10 and above
+		};
+
+		public enum AudioOutput
+		{
+			System,						// Default
+			Unity,						// Media Foundation API only
+			FacebookAudio360,			// Media Foundation API only
+			None,						// Media Foundation API only
+		}
+	}
+
+	public static class Android
+	{
+		public enum VideoApi
+		{
+			MediaPlayer = 1,
+			ExoPlayer,
+		}
+
+		public enum AudioOutput
+		{
+			System,						// Default
+			Unity,						// ExoPlayer API only
+			FacebookAudio360,			// ExoPlayer API only
+		}
+
+		public enum TextureFiltering
+		{
+			Point,
+			Bilinear,
+			Trilinear,
+		}
+
+		public const int Default_MinBufferTimeMs					= 50000;	// Only valid when using ExoPlayer (default comes from DefaultLoadControl.DEFAULT_MIN_BUFFER_MS)
+		public const int Default_MaxBufferTimeMs					= 50000;	// Only valid when using ExoPlayer (default comes from DefaultLoadControl.DEFAULT_MAX_BUFFER_MS)
+		public const int Default_BufferForPlaybackMs				= 2500;		// Only valid when using ExoPlayer (default comes from DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS)
+		public const int Default_BufferForPlaybackAfterRebufferMs	= 5000;		// Only valid when using ExoPlayer (default comes from DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS)
+	}
+
+	public static class WebGL
+	{
+		public enum ExternalLibrary
+		{
+			None,
+			DashJs,
+			HlsJs,
+			Custom,
+		}
+	}
+
+	// Facebook Audio 360 channel mapping
+	public enum Audio360ChannelMode
+	{
+		TBE_8_2 = 0,         /// 8 channels of hybrid TBE ambisonics and 2 channels of head-locked stereo audio
+		TBE_8,               /// 8 channels of hybrid TBE ambisonics. NO head-locked stereo audio
+		TBE_6_2,             /// 6 channels of hybrid TBE ambisonics and 2 channels of head-locked stereo audio
+		TBE_6,               /// 6 channels of hybrid TBE ambisonics. NO head-locked stereo audio
+		TBE_4_2,             /// 4 channels of hybrid TBE ambisonics and 2 channels of head-locked stereo audio
+		TBE_4,               /// 4 channels of hybrid TBE ambisonics. NO head-locked stereo audio
+		TBE_8_PAIR0,         /// Channels 1 and 2 of TBE hybrid ambisonics
+		TBE_8_PAIR1,         /// Channels 3 and 4 of TBE hybrid ambisonics
+		TBE_8_PAIR2,         /// Channels 5 and 6 of TBE hybrid ambisonics
+		TBE_8_PAIR3,         /// Channels 7 and 8 of TBE hybrid ambisonics
+		TBE_CHANNEL0,        /// Channels 1 of TBE hybrid ambisonics
+		TBE_CHANNEL1,        /// Channels 2 of TBE hybrid ambisonics
+		TBE_CHANNEL2,        /// Channels 3 of TBE hybrid ambisonics
+		TBE_CHANNEL3,        /// Channels 4 of TBE hybrid ambisonics
+		TBE_CHANNEL4,        /// Channels 5 of TBE hybrid ambisonics
+		TBE_CHANNEL5,        /// Channels 6 of TBE hybrid ambisonics
+		TBE_CHANNEL6,        /// Channels 7 of TBE hybrid ambisonics
+		TBE_CHANNEL7,        /// Channels 8 of TBE hybrid ambisonics
+		HEADLOCKED_STEREO,   /// Head-locked stereo audio
+		HEADLOCKED_CHANNEL0, /// Channels 1 or left of head-locked stereo audio
+		HEADLOCKED_CHANNEL1, /// Channels 2 or right of head-locked stereo audio
+		AMBIX_4,             /// 4 channels of first order ambiX
+		AMBIX_4_2,           /// 4 channels of first order ambiX with 2 channels of head-locked audio
+		AMBIX_9,             /// 9 channels of second order ambiX
+		AMBIX_9_2,           /// 9 channels of second order ambiX with 2 channels of head-locked audio
+		AMBIX_16,            /// 16 channels of third order ambiX
+		AMBIX_16_2,          /// 16 channels of third order ambiX with 2 channels of head-locked audio
+		MONO,                /// Mono audio
+		STEREO,              /// Stereo audio
+		UNKNOWN,             /// Unknown channel map
+		INVALID,             /// Invalid/unknown map. This must always be last.
+	}
+
+	[System.Flags]
+	public enum AudioChannelMaskFlags : int
+	{
+		Unspecified 		= 0x0,
+		FrontLeft 			= 0x1,
+		FrontRight 			= 0x2,
+		FrontCenter 		= 0x4,
+		LowFrequency 		= 0x8,
+		BackLeft 			= 0x10,
+		BackRight 			= 0x20,
+		FrontLeftOfCenter 	= 0x40,
+		FrontRightOfCenter 	= 0x80,
+		BackCenter 			= 0x100,
+		SideLeft 			= 0x200,
+		SideRight 			= 0x400,
+		TopCenter 			= 0x800,
+		TopFrontLeft 		= 0x1000,
+		TopFrontCenter 		= 0x2000,
+		TopFrontRight 		= 0x4000,
+		TopBackLeft 		= 0x8000,
+		TopBackCenter 		= 0x10000,
+		TopBackRight 		= 0x20000,
+	}
+
+	public enum TextureFlags : int
+	{
+		Unknown = 0,
+		TopDown = 1 << 0,
+		SamplingIsLinear = 1 << 1,
+	}
+
+	[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 1)]
+	public struct BufferedFramesState
+	{
+		public System.Int32 freeFrameCount;
+		public System.Int32 bufferedFrameCount;
+		public System.Int64 minTimeStamp;
+		public System.Int64 maxTimeStamp;
+		public System.Int32 prerolledCount;
+	}
+
+	[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 1)]
+	public struct TextureFrame
+	{
+		internal System.IntPtr texturePointer;
+		internal System.IntPtr auxTexturePointer;
+		internal System.Int64 timeStamp;
+		internal System.UInt32 frameCounter;
+		internal System.UInt32 writtenFrameCount;
+		internal TextureFlags flags;
+		internal System.IntPtr internalNativePointer;
+	}
+
+	[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 1)]
+	public struct TimeRange
+	{
+		public TimeRange(double startTime, double duration)
+		{
+			this.startTime = startTime;
+			this.duration = duration;
+		}
+
+		public double startTime, duration;
+
+		public double StartTime { get { return startTime; } }
+		public double EndTime { get { return startTime + duration; } }
+		public double Duration { get { return duration; } }
+	}
+
+	public class TimeRanges : IEnumerable
+	{
+		internal TimeRanges() {}
+
+		public IEnumerator GetEnumerator()
+		{
+			return _ranges.GetEnumerator();
+		}
+
+		public TimeRange this[int index]
+		{
+			get
+			{
+				return _ranges[index];
+			}
+		}
+
+		internal TimeRanges(TimeRange[] ranges)
+		{
+			_ranges = ranges;
+			CalculateRange();
+		}
+
+		internal void CalculateRange()
+		{
+			_minTime = _maxTime = 0.0;
+			if (_ranges != null && _ranges.Length > 0)
+			{
+				double maxTime = 0.0;
+				double minTime = double.MaxValue;
+				for (int i = 0; i < _ranges.Length; i++)
+				{
+					minTime = System.Math.Min(minTime, _ranges[i].startTime);
+					maxTime = System.Math.Max(maxTime, _ranges[i].startTime + _ranges[i].duration);
+				}
+				_minTime = minTime;
+				_maxTime = maxTime;
+			}
+		}
+
+		public int Count { get { return _ranges.Length; } }
+		public double MinTime { get { return _minTime; } }
+		public double MaxTime { get { return _maxTime; } }
+		public double Duration { get { return (_maxTime - _minTime); } }
+
+		internal TimeRange[] _ranges = new TimeRange[0];
+		internal double _minTime = 0.0;
+		internal double _maxTime = 0.0;
+	}
+
+	/// <summary>
+	/// Video gravity to use with external playback.
+	/// </summary>
+	public enum ExternalPlaybackVideoGravity
+	{
+		/// <summary>Resizes the video to fit the display, may cause stretching.</summary>
+		Resize,
+		/// <summary>Resizes the video whilst preserving the video's aspect ratio to fit the display bounds.</summary>
+		ResizeAspect,
+		/// <summary>Resizes the video whilst preserving aspect to fill the display bounds.</summary>
+		ResizeAspectFill,
+	};
+
+}

+ 12 - 0
Assets/AVProVideo/Runtime/Scripts/Internal/Interfaces.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 00407cbf3ca503142903894431082ac6
+timeCreated: 1438695622
+licenseType: Pro
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 225 - 0
Assets/AVProVideo/Runtime/Scripts/Internal/PlaybackQualityStats.cs

@@ -0,0 +1,225 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	/// <summary>
+	/// Attempts to give insight into video playback presentation smoothness quality
+	/// Keeps track of skipped and duplicated frames and warns about suboptimal setup
+	/// such as no vsync enabled or video frame rate not being a multiple of the display frame rate
+	/// </summary>
+	public class PlaybackQualityStats
+	{
+		public int SkippedFrames { get; private set; }
+		public int DuplicateFrames { get; private set; }
+		public int UnityDroppedFrames { get; private set; }
+		public float PerfectFramesT { get; private set; }
+		public string VSyncStatus { get; private set; }
+		private int PerfectFrames { get; set; }
+		private int TotalFrames { get; set; }
+
+		public bool LogIssues { get; set; }
+
+		private int _sameFrameCount;
+		private long _lastTimeStamp;
+		private BaseMediaPlayer _player;
+
+		public void Reset()
+		{
+			_sameFrameCount = 0;
+			if (_player != null)
+			{
+				_lastTimeStamp = _player.GetTextureTimeStamp();
+			}
+
+			SkippedFrames = 0;
+			DuplicateFrames = 0;
+			UnityDroppedFrames = 0;
+			TotalFrames = 0;
+			PerfectFrames = 0;
+			PerfectFramesT = 0f;
+		}
+
+		internal void Start(BaseMediaPlayer player)
+		{
+			_player = player;
+			Reset();
+
+			bool vsyncEnabled = true;
+			if (QualitySettings.vSyncCount == 0)
+			{
+				vsyncEnabled = false;
+				if (LogIssues)
+				{
+					Debug.LogWarning("[AVProVideo][Quality] VSync is currently disabled in Quality Settings");
+				}
+			}
+			if (!IsGameViewVSyncEnabled())
+			{
+				vsyncEnabled = false;
+				if (LogIssues)
+				{
+					Debug.LogWarning("[AVProVideo][Quality] VSync is currently disabled in the Game View");
+				}
+			}
+
+			float frameRate = _player.GetVideoFrameRate();
+			float frameMs = (1000f / frameRate);
+			if (LogIssues)
+			{
+				Debug.Log(string.Format("[AVProVideo][Quality] Video: {0}fps {1}ms", frameRate, frameMs));
+			}
+
+			if (vsyncEnabled)
+			{
+#if UNITY_2022_2_OR_NEWER
+				float refreshRate = (float)( Screen.currentResolution.refreshRateRatio.value );
+#else
+				float refreshRate = (float)( Screen.currentResolution.refreshRate );
+#endif
+
+				float vsyncRate = refreshRate / QualitySettings.vSyncCount;
+				float vsyncMs = (1000f / vsyncRate);
+
+				if (LogIssues)
+				{
+					Debug.Log(string.Format("[AVProVideo][Quality] VSync: {0}fps {1}ms", vsyncRate, vsyncMs));
+				}
+
+				float framesPerVSync = frameMs / vsyncMs;
+				float fractionalframesPerVsync = framesPerVSync - Mathf.FloorToInt(framesPerVSync);
+				if (fractionalframesPerVsync > 0.0001f && LogIssues)
+				{
+					Debug.LogWarning("[AVProVideo][Quality] Video is not a multiple of VSync so playback cannot be perfect");
+				}
+				VSyncStatus = "VSync " + framesPerVSync;
+			}
+			else
+			{
+				if (LogIssues)
+				{
+					Debug.LogWarning("[AVProVideo][Quality] Running without VSync enabled");
+				}
+				VSyncStatus = "No VSync";
+			}
+		}
+
+		internal void Update()
+		{
+			if (_player == null) return;
+
+			// Don't analyse stats unless real playback is happening
+			if (_player.IsPaused() || _player.IsSeeking() || _player.IsFinished()) return;
+
+			long timeStamp = _player.GetTextureTimeStamp();
+			long frameDuration = (long)(Helper.SecondsToHNS / _player.GetVideoFrameRate());
+
+			bool isPerfectFrame = true;
+
+			// Check for skipped frames
+			long d = (timeStamp - _lastTimeStamp);
+			if (d > 0)
+			{
+				const long threshold = 10000;
+				d -= frameDuration;
+				if (d > threshold)
+				{
+					int skippedFrames = Mathf.FloorToInt((float)d / (float)frameDuration);
+					if (LogIssues)
+					{
+						Debug.LogWarning("[AVProVideo][Quality] Possible frame skip, at " + timeStamp + " delta " + d + " = " + skippedFrames + " frames");
+					}
+					SkippedFrames += skippedFrames;
+					isPerfectFrame = false;
+				}
+			}
+
+			if (QualitySettings.vSyncCount != 0)
+			{
+#if UNITY_2022_2_OR_NEWER
+				float refreshRate = (float)( Screen.currentResolution.refreshRateRatio.value );
+#else
+				float refreshRate = (float)( Screen.currentResolution.refreshRate );
+#endif
+
+				long vsyncDuration = (long)((QualitySettings.vSyncCount * Helper.SecondsToHNS) / refreshRate);
+				if (timeStamp != _lastTimeStamp)
+				{
+					float framesPerVSync = (float)frameDuration / (float)vsyncDuration;
+					//Debug.Log((float)frameDuration + " " +  (float)vsyncDuration);
+					float fractionalFramesPerVSync = framesPerVSync - Mathf.FloorToInt(framesPerVSync);
+
+					//Debug.Log(framesPerVSync + " " + fractionalFramesPerVSync);
+					// VSync rate is a multiple of the video rate so we should be able to get perfectly smooth playback
+					if (fractionalFramesPerVSync <= 0.0001f)
+					{
+						// Check for duplicate frames
+						if (!Mathf.Approximately(_sameFrameCount, (int)framesPerVSync))
+						{
+							if (LogIssues)
+							{
+								Debug.LogWarning("[AVProVideo][Quality] Frame " + timeStamp + " was shown for " + _sameFrameCount + " frames instead of expected " + framesPerVSync);
+							}
+							DuplicateFrames++;
+							isPerfectFrame = false;
+						}
+					}
+
+					_sameFrameCount = 1;
+				}
+				else
+				{
+					// Count the number of Unity-frames the video-frame is displayed for
+					_sameFrameCount++;
+				}
+
+				// Check for Unity dropping frames
+				{
+					long frameTime = (long)(Time.deltaTime * Helper.SecondsToHNS);
+					if (frameTime > (vsyncDuration + (vsyncDuration / 3)))
+					{
+						if (LogIssues)
+						{
+							Debug.LogWarning("[AVProVideo][Quality] Possible Unity dropped frame, delta time: " + (Time.deltaTime * 1000f) + "ms");
+						}
+						UnityDroppedFrames++;
+						isPerfectFrame = false;
+					}
+				}
+			}
+
+			if (_lastTimeStamp != timeStamp)
+			{
+				if (isPerfectFrame)
+				{
+					PerfectFrames++;
+				}
+				TotalFrames++;
+				PerfectFramesT = (float)PerfectFrames / (float)TotalFrames;
+			}
+
+			_lastTimeStamp = timeStamp;
+		}
+
+		private static bool IsGameViewVSyncEnabled()
+		{
+			bool result = true;
+#if UNITY_EDITOR && UNITY_2019_1_OR_NEWER
+			System.Reflection.Assembly assembly = typeof(UnityEditor.EditorWindow).Assembly;
+			System.Type type = assembly.GetType("UnityEditor.GameView");
+			UnityEditor.EditorWindow window = UnityEditor.EditorWindow.GetWindow(type);
+			System.Reflection.PropertyInfo prop = type.GetProperty("vSyncEnabled");
+			if (prop != null)
+			{
+				result = (bool)prop.GetValue(window);
+			}
+#endif
+			return result;
+		}
+	}
+}

+ 12 - 0
Assets/AVProVideo/Runtime/Scripts/Internal/PlaybackQualityStats.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 4f344d823db9c4148af4dec2235f690d
+timeCreated: 1634119397
+licenseType: Pro
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 7 - 0
Assets/AVProVideo/Runtime/Scripts/Internal/Players.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: e2f8dd08c4c77654282b755fd4a069c1
+folderAsset: yes
+DefaultImporter:
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 1725 - 0
Assets/AVProVideo/Runtime/Scripts/Internal/Players/AndroidMediaPlayer.cs

@@ -0,0 +1,1725 @@
+// NOTE: We only allow this script to compile in editor so we can easily check for compilation issues
+#if (UNITY_EDITOR || UNITY_ANDROID)
+
+#define AVPROVIDEO_FIXREGRESSION_TEXTUREQUALITY_UNITY542
+#define DLL_METHODS
+
+using UnityEngine;
+using System;
+using System.Text;
+using System.Runtime.InteropServices;
+
+//-----------------------------------------------------------------------------
+// Copyright 2015-2021 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+namespace RenderHeads.Media.AVProVideo
+{
+	/// <summary>
+	/// Android implementation of BaseMediaPlayer
+	/// </summary>
+	// TODO: seal this class
+	public class AndroidMediaPlayer : BaseMediaPlayer
+	{
+		protected static AndroidJavaObject	s_ActivityContext	= null;
+		protected static AndroidJavaObject  s_Interface			= null;
+		protected static bool				s_bInitialised		= false;
+
+		private static string				s_Version = "Plug-in not yet initialised";
+
+		private static System.IntPtr 		_nativeFunction_RenderEvent = System.IntPtr.Zero;
+
+		protected AndroidJavaObject			m_Video;
+		private Texture2D					m_Texture;
+		private int                         m_TextureHandle;
+		private bool						m_UseFastOesPath;
+
+//		private string						m_AuthToken;
+
+		private double						m_Duration			= 0.0;
+		private int							m_Width				= 0;
+		private int							m_Height			= 0;
+
+		protected int 						m_iPlayerIndex		= -1;
+
+		private Android.VideoApi			m_API;
+		private bool						m_HeadRotationEnabled = false;
+		private bool						m_FocusEnabled = false;
+		private System.IntPtr 				m_Method_Update;
+		private System.IntPtr 				m_Method_SetHeadRotation;
+		private System.IntPtr				m_Method_GetCurrentTimeS;
+		private System.IntPtr				m_Method_GetSourceVideoFrameRate;
+		private System.IntPtr				m_Method_IsPlaying;
+		private System.IntPtr				m_Method_IsPaused;
+		private System.IntPtr				m_Method_IsFinished;
+		private System.IntPtr				m_Method_IsSeeking;
+		private System.IntPtr				m_Method_IsBuffering;
+		private System.IntPtr				m_Method_IsLooping;
+		private System.IntPtr				m_Method_HasVideo;
+		private System.IntPtr				m_Method_HasAudio;
+		private System.IntPtr               m_Method_HasMetaData;
+		private System.IntPtr				m_Method_SetFocusProps;
+		private System.IntPtr				m_Method_SetFocusEnabled;
+		private System.IntPtr				m_Method_SetFocusRotation;
+		private jvalue[]					m_Value0 = new jvalue[0];
+		private jvalue[]					m_Value1 = new jvalue[1];
+		private jvalue[]					m_Value2 = new jvalue[2];
+		private jvalue[]					m_Value4 = new jvalue[4];
+
+		private MediaPlayer.OptionsAndroid	m_Options;
+
+		private enum NativeStereoPacking : int
+		{
+			Unknown = -1,		// Unknown
+			Monoscopic = 0,		// Monoscopic
+			TopBottom = 1,		// Top is the left eye, bottom is the right eye
+			LeftRight = 2,		// Left is the left eye, right is the right eye
+			Mesh = 3,			// Use the mesh UV to unpack, uv0=left eye, uv1=right eye
+		}
+
+#if AVPROVIDEO_FIXREGRESSION_TEXTUREQUALITY_UNITY542
+	#if UNITY_2022_2_OR_NEWER
+		// Note: See https://docs.unity3d.com/2022.2/Documentation/ScriptReference/QualitySettings-masterTextureLimit.html
+		private int _textureQuality = QualitySettings.globalTextureMipmapLimit;
+	#else
+		private int _textureQuality = QualitySettings.masterTextureLimit;
+	#endif
+#endif
+		static private System.Threading.Thread		m_MainThread;
+
+		public static bool InitialisePlatform()
+		{
+			m_MainThread = System.Threading.Thread.CurrentThread;
+
+			// Get the activity context
+			if (s_ActivityContext == null)
+			{
+				AndroidJavaClass activityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
+				if (activityClass != null)
+				{
+					s_ActivityContext = activityClass.GetStatic<AndroidJavaObject>("currentActivity");
+				}
+			}
+
+			if (!s_bInitialised)
+			{
+				s_Interface = new AndroidJavaObject("com.renderheads.AVPro.Video.Manager");
+				if (s_Interface != null)
+				{
+					s_Version = s_Interface.Call<string>("GetPluginVersion");
+					s_Interface.Call("SetContext", s_ActivityContext);
+
+					// Calling this native function cause the .SO library to become loaded
+					// This is important for Unity < 5.2.0 where GL.IssuePluginEvent works differently
+					_nativeFunction_RenderEvent = Native.GetRenderEventFunc();
+					if (_nativeFunction_RenderEvent != IntPtr.Zero)
+					{
+						s_bInitialised = true;
+					}
+				}
+			}
+
+			return s_bInitialised;
+		}
+
+		public static void DeinitPlatform()
+		{
+			if (s_bInitialised)
+			{
+				if (s_Interface != null)
+				{
+					s_Interface.CallStatic("Deinitialise");
+					s_Interface = null;
+				}
+				s_ActivityContext = null;
+				s_bInitialised = false;
+			}
+		}
+
+		private static void IssuePluginEvent(Native.AVPPluginEvent type, int param)
+		{
+			// Build eventId from the type and param.
+			int eventId = 0x5d5ac000 | ((int)type << 8);
+
+			switch (type)
+			{
+				case Native.AVPPluginEvent.PlayerSetup:
+				case Native.AVPPluginEvent.PlayerUpdate:
+				case Native.AVPPluginEvent.PlayerDestroy:
+				case Native.AVPPluginEvent.ExtractFrame:
+					{
+						eventId |= param & 0xff;
+					}
+					break;
+			}
+
+			GL.IssuePluginEvent(_nativeFunction_RenderEvent, eventId);
+		}
+
+		private System.IntPtr GetMethod(string methodName, string signature)
+		{
+			Debug.Assert(m_Video != null);
+			System.IntPtr result = AndroidJNIHelper.GetMethodID(m_Video.GetRawClass(), methodName, signature, false);
+
+			Debug.Assert(result != System.IntPtr.Zero);
+			if (result == System.IntPtr.Zero)
+			{
+				Debug.LogError("[AVProVideo] Unable to get method " + methodName + " " + signature);
+				throw new System.Exception("[AVProVideo] Unable to get method " + methodName + " " + signature);
+			}
+
+			return result;
+		}
+
+		public AndroidMediaPlayer(MediaPlayer.OptionsAndroid options) : base()
+		{
+			Debug.Assert(s_Interface != null);
+			Debug.Assert(s_bInitialised);
+
+			m_Options = options;
+
+			m_API = options.videoApi;
+
+			if (SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.Vulkan)
+			{
+				Debug.LogWarning("[AVProVideo] Vulkan graphics API is not supported.  Please select OpenGL ES 2.0 and 3.0 are supported on Android.");
+			}
+
+#if UNITY_2017_2_OR_NEWER
+			Vector2 vPreferredVideo = GetPreferredVideoResolution(options.preferredMaximumResolution, options.customPreferredMaximumResolution);
+#else
+			Vector2 vPreferredVideo = GetPreferredVideoResolution(options.preferredMaximumResolution, new Vector2(0.0f, 0.0f));
+#endif
+
+			// Create a java-size video class up front
+			Debug.Log("s_Interface " + s_Interface);
+			m_Video = s_Interface.Call<AndroidJavaObject>( "CreatePlayer", (int)(m_API), options.useFastOesPath, options.preferSoftwareDecoder, (int)(options.audioOutput), (int)(options.audio360ChannelMode), Helper.GetUnityAudioSampleRate(), 
+																		   options.StartWithHighestBandwidth(), options.minBufferMs, options.maxBufferMs, options.bufferForPlaybackMs, options.bufferForPlaybackAfterRebufferMs, 
+																		   (int)(options.GetPreferredPeakBitRateInBitsPerSecond()), (int)(vPreferredVideo.x), (int)(vPreferredVideo.y), (int)(options.blitTextureFiltering) );
+			Debug.Log("m_Video " + m_Video);
+
+			if (m_Video != null)
+			{
+				m_Method_Update = GetMethod("Update", "()V");
+				m_Method_SetHeadRotation = GetMethod("SetHeadRotation", "(FFFF)V");
+				m_Method_SetFocusProps = GetMethod("SetFocusProps", "(FF)V");
+				m_Method_SetFocusEnabled = GetMethod("SetFocusEnabled", "(Z)V");
+				m_Method_SetFocusRotation = GetMethod("SetFocusRotation", "(FFFF)V");
+				m_Method_GetCurrentTimeS = GetMethod("GetCurrentTimeS", "()D");
+				m_Method_GetSourceVideoFrameRate = GetMethod("GetSourceVideoFrameRate", "()F");
+				m_Method_IsPlaying = GetMethod("IsPlaying", "()Z");
+				m_Method_IsPaused = GetMethod("IsPaused", "()Z");
+				m_Method_IsFinished = GetMethod("IsFinished", "()Z");
+				m_Method_IsSeeking = GetMethod("IsSeeking", "()Z");
+				m_Method_IsBuffering = GetMethod("IsBuffering", "()Z");
+				m_Method_IsLooping = GetMethod("IsLooping", "()Z");
+				m_Method_HasVideo = GetMethod("HasVideo", "()Z");
+				m_Method_HasAudio = GetMethod("HasAudio", "()Z");
+				m_Method_HasMetaData = GetMethod("HasMetaData", "()Z");
+
+				m_iPlayerIndex = m_Video.Call<int>("GetPlayerIndex");
+				Helper.LogInfo("Creating player " + m_iPlayerIndex);
+				SetOptions(options.useFastOesPath, options.showPosterFrame);
+
+				// Initialise renderer, on the render thread
+				AndroidMediaPlayer.IssuePluginEvent(Native.AVPPluginEvent.PlayerSetup, m_iPlayerIndex);
+			}
+			else
+			{
+				Debug.LogError("[AVProVideo] Failed to create player instance");
+			}
+		}
+
+		public void SetOptions(bool useFastOesPath, bool showPosterFrame)
+		{
+			m_UseFastOesPath = useFastOesPath;
+
+			if (m_Video != null)
+			{
+				// Show poster frame is only needed when using the MediaPlayer API
+				showPosterFrame = (m_API == Android.VideoApi.MediaPlayer) ? showPosterFrame:false;
+
+				m_Video.Call("SetPlayerOptions", m_UseFastOesPath, showPosterFrame);
+			}
+		}
+
+		public override long GetEstimatedTotalBandwidthUsed()
+		{
+			long result = -1;
+			if (s_Interface != null)
+			{
+				result = m_Video.Call<long>("GetEstimatedBandwidthUsed");
+			}
+			return result;
+		}
+
+
+		public override string GetVersion()
+		{
+			return s_Version;
+		}
+
+		public override string GetExpectedVersion()
+		{
+			return Helper.ExpectedPluginVersion.Android;
+		}
+
+		public override bool OpenMedia(string path, long offset, string httpHeader, MediaHints mediaHints, int forceFileFormat = 0, bool startWithHighestBitrate = false)
+		{
+			bool bReturn = false;
+
+			if (m_Video != null)
+			{
+				_mediaHints = mediaHints;
+
+				Debug.Assert(m_Width == 0 && m_Height == 0 && m_Duration == 0.0);
+				bReturn = m_Video.Call<bool>("OpenVideoFromFile", path, offset, httpHeader, forceFileFormat, (int)(m_Options.audioOutput), (int)(m_Options.audio360ChannelMode));
+				if (!bReturn)
+				{
+					DisplayLoadFailureSuggestion(path);
+				}
+			}
+			else
+			{
+				Debug.LogError("[AVProVideo] m_Video is null!");
+			}
+
+			return bReturn;
+		}
+
+		public override void SetKeyServerAuthToken(string token)
+		{
+			if (m_Video != null)
+			{
+				m_Video.Call("SetKeyServerAuthToken", token);
+			}
+		}
+
+		public override void SetOverrideDecryptionKey(byte[] key)
+		{
+			if( m_Video != null )
+			{
+				m_Video.Call("SetOverrideDecryptionKey", key);
+			}
+		}
+
+		private void DisplayLoadFailureSuggestion(string path)
+		{
+			if (path.ToLower().Contains("http://"))
+			{
+				Debug.LogError("Android 8 and above require HTTPS by default, change to HTTPS or enable ClearText in the AndroidManifest.xml");
+			}
+		}
+
+		public override void CloseMedia()
+		{
+			if (m_Texture != null)
+			{
+				Texture2D.Destroy(m_Texture);
+				m_Texture = null;
+			}
+			m_TextureHandle = 0;
+
+			m_Duration = 0.0;
+			m_Width = 0;
+			m_Height = 0;
+
+			if (m_Video != null)
+			{
+				m_Video.Call("CloseVideo");
+			}
+
+			base.CloseMedia();
+		}
+
+		public override void SetLooping( bool bLooping )
+		{
+			if( m_Video != null )
+			{
+				m_Video.Call("SetLooping", bLooping);
+			}
+		}
+
+		public override bool IsLooping()
+		{
+			bool result = false;
+			if( m_Video != null )
+			{
+				if (m_Method_IsLooping != System.IntPtr.Zero)
+				{
+					result = AndroidJNI.CallBooleanMethod(m_Video.GetRawObject(), m_Method_IsLooping, m_Value0);
+				}
+				else
+				{
+					result = m_Video.Call<bool>("IsLooping");
+				}
+			}
+			return result;
+		}
+
+		public override bool HasVideo()
+		{
+			bool result = false;
+			if( m_Video != null )
+			{
+				if (m_Method_HasVideo != System.IntPtr.Zero)
+				{
+					result = AndroidJNI.CallBooleanMethod(m_Video.GetRawObject(), m_Method_HasVideo, m_Value0);
+				}
+				else
+				{
+					result = m_Video.Call<bool>("HasVideo");
+				}
+			}
+			return result;
+		}
+
+		public override bool HasAudio()
+		{
+			bool result = false;
+			if( m_Video != null )
+			{
+				if (m_Method_HasAudio != System.IntPtr.Zero)
+				{
+					result = AndroidJNI.CallBooleanMethod(m_Video.GetRawObject(), m_Method_HasAudio, m_Value0);
+				}
+				else
+				{
+					result = m_Video.Call<bool>("HasAudio");
+				}
+			}
+			return result;
+		}
+
+		public override bool HasMetaData()
+		{
+			bool result = false;
+			if (m_Video != null)
+			{
+				if (m_Method_HasMetaData != System.IntPtr.Zero)
+				{
+					result = AndroidJNI.CallBooleanMethod(m_Video.GetRawObject(), m_Method_HasMetaData, m_Value0);
+				}
+				else
+				{
+					result = m_Video.Call<bool>("HasMetaData");
+				}
+			}
+			return result;
+		}
+
+		public override bool CanPlay()
+		{
+			bool result = false;
+#if DLL_METHODS
+			result = Native._CanPlay( m_iPlayerIndex );
+#else
+			if (m_Video != null)
+			{
+				result = m_Video.Call<bool>("CanPlay");
+			}
+#endif
+			return result;
+		}
+
+		public override void Play()
+		{
+			if (m_Video != null)
+			{
+				m_Video.Call("Play");
+			}
+		}
+
+		public override void Pause()
+		{
+			if (m_Video != null)
+			{
+				m_Video.Call("Pause");
+			}
+		}
+
+		public override void Stop()
+		{
+			if (m_Video != null)
+			{
+				// On Android we never need to actually Stop the playback, pausing is fine
+				m_Video.Call("Pause");
+			}
+		}
+
+		public override void Seek(double time)
+		{
+			if (m_Video != null)
+			{
+				// time is in seconds
+				m_Video.Call("Seek", time);
+			}
+		}
+
+		public override void SeekFast(double time)
+		{
+			if (m_Video != null)
+			{
+				// time is in seconds
+				m_Video.Call("SeekFast", time);
+			}
+		}
+
+		public override double GetCurrentTime()
+		{
+			double result = 0.0;
+			if (m_Video != null)
+			{
+				if (m_Method_GetCurrentTimeS != System.IntPtr.Zero)
+				{
+					result = AndroidJNI.CallDoubleMethod(m_Video.GetRawObject(), m_Method_GetCurrentTimeS, m_Value0);
+				}
+				else
+				{
+					result = (double)m_Video.Call<double>("GetCurrentTimeS");
+				}
+			}
+			return result;
+		}
+
+		public override void SetPlaybackRate(float rate)
+		{
+			if (m_Video != null)
+			{
+				m_Video.Call("SetPlaybackRate", rate);
+			}
+		}
+
+		public override float GetPlaybackRate()
+		{
+			float result = 0.0f;
+			if (m_Video != null)
+			{
+				result = m_Video.Call<float>("GetPlaybackRate");
+			}
+			return result;
+		}
+
+		public override void SetAudioHeadRotation(Quaternion q)
+		{
+			if (m_Video != null)
+			{
+				if (!m_HeadRotationEnabled)
+				{
+					m_Video.Call("SetPositionTrackingEnabled", true);
+					m_HeadRotationEnabled = true;
+				}
+
+				if (m_Method_SetHeadRotation != System.IntPtr.Zero)
+				{
+					m_Value4[0].f = q.x;
+					m_Value4[1].f = q.y;
+					m_Value4[2].f = q.z;
+					m_Value4[3].f = q.w;
+					AndroidJNI.CallVoidMethod(m_Video.GetRawObject(), m_Method_SetHeadRotation, m_Value4);
+				}
+				else
+				{
+					m_Video.Call("SetHeadRotation", q.x, q.y, q.z, q.w);
+				}
+			}
+		}
+
+		public override void ResetAudioHeadRotation()
+		{
+			if (m_Video != null && m_HeadRotationEnabled)
+			{
+				m_Video.Call("SetPositionTrackingEnabled", false);
+				m_HeadRotationEnabled = false;
+			}
+		}
+
+		public override void SetAudioFocusEnabled(bool enabled)
+		{
+			if (m_Video != null && enabled != m_FocusEnabled)
+			{
+				if (m_Method_SetFocusEnabled != System.IntPtr.Zero)
+				{
+					m_Value1[0].z = enabled;
+					AndroidJNI.CallVoidMethod(m_Video.GetRawObject(), m_Method_SetFocusEnabled, m_Value1);
+				}
+				else
+				{
+					m_Video.Call("SetFocusEnabled", enabled);
+				}
+				m_FocusEnabled = enabled;
+			}
+		}
+
+		public override void SetAudioFocusProperties(float offFocusLevel, float widthDegrees)
+		{
+			if(m_Video != null && m_FocusEnabled)
+			{
+				if (m_Method_SetFocusProps != System.IntPtr.Zero)
+				{
+					m_Value2[0].f = offFocusLevel;
+					m_Value2[1].f = widthDegrees;
+					AndroidJNI.CallVoidMethod(m_Video.GetRawObject(), m_Method_SetFocusProps, m_Value2);
+				}
+				else
+				{
+					m_Video.Call("SetFocusProps", offFocusLevel, widthDegrees);
+				}
+			}
+		}
+
+		public override void SetAudioFocusRotation(Quaternion q)
+		{
+			if (m_Video != null && m_FocusEnabled)
+			{
+				if (m_Method_SetFocusRotation != System.IntPtr.Zero)
+				{
+					m_Value4[0].f = q.x;
+					m_Value4[1].f = q.y;
+					m_Value4[2].f = q.z;
+					m_Value4[3].f = q.w;
+					AndroidJNI.CallVoidMethod(m_Video.GetRawObject(), m_Method_SetFocusRotation, m_Value4);
+				}
+				else
+				{
+					m_Video.Call("SetFocusRotation", q.x, q.y, q.z, q.w);
+				}
+			}
+		}
+
+		public override void ResetAudioFocus()
+		{
+			if (m_Video != null)
+			{
+				if (m_Method_SetFocusProps != System.IntPtr.Zero &&
+					m_Method_SetFocusEnabled != System.IntPtr.Zero &&
+					m_Method_SetFocusRotation != System.IntPtr.Zero)
+				{
+					m_Value2[0].f = 0f;
+					m_Value2[1].f = 90f;
+					AndroidJNI.CallVoidMethod(m_Video.GetRawObject(), m_Method_SetFocusProps, m_Value2);
+					m_Value1[0].z = false;
+					AndroidJNI.CallVoidMethod(m_Video.GetRawObject(), m_Method_SetFocusEnabled, m_Value1);
+					m_Value4[0].f = 0f;
+					m_Value4[1].f = 0f;
+					m_Value4[2].f = 0f;
+					m_Value4[3].f = 1f;
+					AndroidJNI.CallVoidMethod(m_Video.GetRawObject(), m_Method_SetFocusRotation, m_Value4);
+				}
+				else
+				{
+					m_Video.Call("SetFocusProps", 0f, 90f);
+					m_Video.Call("SetFocusEnabled", false);
+					m_Video.Call("SetFocusRotation", 0f, 0f, 0f, 1f);
+				}
+			}
+		}
+
+		public override double GetDuration()
+		{
+			return m_Duration;
+		}
+
+		public override int GetVideoWidth()
+		{
+			return m_Width;
+		}
+			
+		public override int GetVideoHeight()
+		{
+			return m_Height;
+		}
+
+		public override float GetVideoFrameRate()
+		{
+			float result = 0.0f;
+			if( m_Video != null )
+			{
+				if (m_Method_GetSourceVideoFrameRate != System.IntPtr.Zero)
+				{
+					result = AndroidJNI.CallFloatMethod(m_Video.GetRawObject(), m_Method_GetSourceVideoFrameRate, m_Value0);
+				}
+				else
+				{
+					result = m_Video.Call<float>("GetSourceVideoFrameRate");
+				}
+			}
+			return result;
+		}
+
+		public override float GetVideoDisplayRate()
+		{
+			float result = 0.0f;
+#if DLL_METHODS
+			result = Native._GetVideoDisplayRate( m_iPlayerIndex );
+#else
+			if (m_Video != null)
+			{
+				result = m_Video.Call<float>("GetDisplayRate");
+			}
+#endif
+			return result;
+		}
+
+		public override bool IsSeeking()
+		{
+			bool result = false;
+			if (m_Video != null)
+			{
+				if (m_Method_IsSeeking != System.IntPtr.Zero)
+				{
+					result = AndroidJNI.CallBooleanMethod(m_Video.GetRawObject(), m_Method_IsSeeking, m_Value0);
+				}
+				else
+				{
+					result = m_Video.Call<bool>("IsSeeking");
+				}
+			}
+			return result;
+		}
+
+		public override bool IsPlaying()
+		{
+			bool result = false;
+			if (m_Video != null)
+			{
+				if (m_Method_IsPlaying != System.IntPtr.Zero)
+				{
+					result = AndroidJNI.CallBooleanMethod(m_Video.GetRawObject(), m_Method_IsPlaying, m_Value0);
+				}
+				else
+				{
+					result = m_Video.Call<bool>("IsPlaying");
+				}
+			}
+			return result;
+		}
+
+		public override bool IsPaused()
+		{
+			bool result = false;
+			if (m_Video != null)
+			{
+				if (m_Method_IsPaused != System.IntPtr.Zero)
+				{
+					result = AndroidJNI.CallBooleanMethod(m_Video.GetRawObject(), m_Method_IsPaused, m_Value0);
+				}
+				else
+				{
+					result = m_Video.Call<bool>("IsPaused");
+				}
+			}
+			return result;
+		}
+
+		public override bool IsFinished()
+		{
+			bool result = false;
+			if (m_Video != null)
+			{
+				if (m_Method_IsFinished != System.IntPtr.Zero)
+				{
+					result = AndroidJNI.CallBooleanMethod(m_Video.GetRawObject(), m_Method_IsFinished, m_Value0);
+				}
+				else
+				{
+					result = m_Video.Call<bool>("IsFinished");
+				}
+			}
+			return result;
+		}
+
+		public override bool IsBuffering()
+		{
+			bool result = false;
+			if (m_Video != null)
+			{
+				if (m_Method_IsBuffering != System.IntPtr.Zero)
+				{
+					result = AndroidJNI.CallBooleanMethod(m_Video.GetRawObject(), m_Method_IsBuffering, m_Value0);
+				}
+				else
+				{
+					result = m_Video.Call<bool>("IsBuffering");
+				}
+			}
+			return result;
+		}
+
+		public override Texture GetTexture( int index )
+		{
+			Texture result = null;
+			if (GetTextureFrameCount() > 0)
+			{
+				result = m_Texture;
+			}
+			return result;
+		}
+
+		public override int GetTextureFrameCount()
+		{
+			int result = 0;
+			if( m_Texture != null )
+			{
+#if DLL_METHODS
+				result = Native._GetFrameCount( m_iPlayerIndex );
+#else
+				if (m_Video != null)
+				{
+					result = m_Video.Call<int>("GetFrameCount");
+				}
+#endif
+			}
+			return result;
+		}
+
+		internal override StereoPacking InternalGetTextureStereoPacking()
+		{
+			StereoPacking result = StereoPacking.Unknown;
+			if (m_Video != null)
+			{
+				NativeStereoPacking internalStereoMode = (NativeStereoPacking)( m_Video.Call<int>("GetCurrentVideoTrackStereoMode") );
+				switch( internalStereoMode )
+				{
+					case NativeStereoPacking.Monoscopic:	result = StereoPacking.None;		break;
+					case NativeStereoPacking.TopBottom:		result = StereoPacking.TopBottom;	break;
+					case NativeStereoPacking.LeftRight:		result = StereoPacking.LeftRight;	break;
+					case NativeStereoPacking.Mesh:			result = StereoPacking.Unknown;		break;
+				}
+			}
+			return result;
+		}
+
+		public override bool RequiresVerticalFlip()
+		{
+			return false;
+		}
+
+		public override void MuteAudio(bool bMuted)
+		{
+			if (m_Video != null)
+			{
+				m_Video.Call("MuteAudio", bMuted);
+			}
+		}
+
+		public override bool IsMuted()
+		{
+			bool result = false;
+			if( m_Video != null )
+			{
+				result = m_Video.Call<bool>("IsMuted");
+			}
+			return result;
+		}
+
+		public override void SetVolume(float volume)
+		{
+			if (m_Video != null)
+			{
+				m_Video.Call("SetVolume", volume);
+			}
+		}
+
+		public override float GetVolume()
+		{
+			float result = 0.0f;
+			if( m_Video != null )
+			{
+				result = m_Video.Call<float>("GetVolume");
+			}
+			return result;
+		}
+
+		public override void SetBalance(float balance)
+		{
+			if( m_Video != null )
+			{
+				m_Video.Call("SetAudioPan", balance);
+			}
+		}
+
+		public override float GetBalance()
+		{
+			float result = 0.0f;
+			if( m_Video != null )
+			{
+				result = m_Video.Call<float>("GetAudioPan");
+			}
+			return result;
+		}
+
+		public override bool WaitForNextFrame(Camera dummyCamera, int previousFrameCount)
+		{
+			// Mark as extracting
+			bool isMultiThreaded = m_Video.Call<bool>("StartExtractFrame");
+
+			// In single threaded Android this method won't work, so just return
+			if (isMultiThreaded)
+			{
+				// Queue up render thread event to wait for the new frame
+				IssuePluginEvent(Native.AVPPluginEvent.ExtractFrame, m_iPlayerIndex);
+
+				// Force render thread to run
+				dummyCamera.Render();
+
+				// Wait for the frame to change
+				m_Video.Call("WaitForExtract");
+
+				// Return whether the frame changed
+				return (previousFrameCount != GetTextureFrameCount());
+			}
+			return false;	
+		}
+
+		public override long GetTextureTimeStamp()
+		{
+			long timeStamp = long.MinValue;
+			if (m_Video != null)
+			{
+				timeStamp = m_Video.Call<long>("GetTextureTimeStamp");
+			}
+			return timeStamp;
+		}
+
+		public override void Render()
+		{
+			if (m_Video != null)
+			{
+				if (m_UseFastOesPath)
+				{
+					// This is needed for at least Unity 5.5.0, otherwise it just renders black in OES mode
+					GL.InvalidateState();
+				}
+				AndroidMediaPlayer.IssuePluginEvent( Native.AVPPluginEvent.PlayerUpdate, m_iPlayerIndex );
+				if (m_UseFastOesPath)
+				{
+					GL.InvalidateState();
+				}
+			}
+		}
+
+		public override void OnEnable()
+		{
+			base.OnEnable();
+
+#if DLL_METHODS
+			int textureHandle = Native._GetTextureHandle(m_iPlayerIndex);
+#else
+			int textureHandle = m_Video.Call<int>("GetTextureHandle");
+#endif
+
+			if (m_Texture != null && textureHandle > 0 && m_Texture.GetNativeTexturePtr() == System.IntPtr.Zero)
+			{
+				//Debug.Log("RECREATING");
+				m_Texture.UpdateExternalTexture(new System.IntPtr(textureHandle));
+			}
+
+#if AVPROVIDEO_FIXREGRESSION_TEXTUREQUALITY_UNITY542
+	#if UNITY_2022_2_OR_NEWER
+		// Note: See https://docs.unity3d.com/2022.2/Documentation/ScriptReference/QualitySettings-masterTextureLimit.html
+			_textureQuality = QualitySettings.globalTextureMipmapLimit;
+	#else
+			_textureQuality = QualitySettings.masterTextureLimit;
+	#endif
+#endif
+		}
+
+		public override System.DateTime GetProgramDateTime()
+		{
+			System.DateTime result;
+			if (m_Video != null)
+			{
+				double seconds = m_Video.Call<double>("GetCurrentAbsoluteTimestamp");
+				result = Helper.ConvertSecondsSince1970ToDateTime(seconds);
+			}
+			else
+			{
+				result = base.GetProgramDateTime();
+			}
+			return result;
+		}
+
+		public override void Update()
+		{
+			if (m_Video != null)
+			{
+				if (m_Method_Update != System.IntPtr.Zero)
+				{
+					AndroidJNI.CallVoidMethod(m_Video.GetRawObject(), m_Method_Update, m_Value0);
+				}
+				else
+				{
+					m_Video.Call("Update");
+				}
+
+//				_lastError = (ErrorCode)( m_Video.Call<int>("GetLastErrorCode") );
+				_lastError = (ErrorCode)( Native._GetLastErrorCode( m_iPlayerIndex ) );
+			}
+
+			if( m_Options.HasChanged( MediaPlayer.OptionsAndroid.ChangeFlags.All, true ) )
+			{
+				if (m_Video != null)
+				{
+#if UNITY_2017_2_OR_NEWER
+					Vector2 vPreferredVideo = GetPreferredVideoResolution(m_Options.preferredMaximumResolution, m_Options.customPreferredMaximumResolution);
+#else
+					Vector2 vPreferredVideo = GetPreferredVideoResolution(m_Options.preferredMaximumResolution, new Vector2(0.0f, 0.0f));
+#endif
+
+					int iNewBitrate = (int)(m_Options.GetPreferredPeakBitRateInBitsPerSecond());
+					/*bool bSetMaxResolutionAndBitrate =*/ m_Video.Call<bool>("SetPreferredVideoResolutionAndBitrate", (int)(vPreferredVideo.x), (int)(vPreferredVideo.y), iNewBitrate);
+				}
+			}
+
+/*
+			m_fTestTime += Time.deltaTime;
+			if( m_fTestTime > 4.0f )
+			{
+				m_fTestTime = 0.0f;
+
+				int iNumStreams = InternalGetAdaptiveStreamCount( TrackType.Video );
+				int iNewStreamIndex = UnityEngine.Random.Range( -1, iNumStreams );
+				SetVideoAdaptiveStreamIndex( TrackType.Video, iNewStreamIndex );
+			}
+*/
+
+			// Call before the native update call
+			UpdateTracks();
+			UpdateTextCue();
+
+			UpdateSubtitles();
+
+			UpdateTimeRanges();
+
+			UpdateDisplayFrameRate();
+
+			if (Mathf.Approximately((float)m_Duration, 0f))
+			{
+#if DLL_METHODS
+				m_Duration = (double)( Native._GetDuration( m_iPlayerIndex ) );
+#else
+				m_Duration = m_Video.Call<double>("GetDurationS");
+#endif
+
+//				if( m_DurationMs > 0.0f ) { Helper.LogInfo("Duration: " + m_DurationMs); }
+			}
+
+			// Check if we can create the texture
+			// Scan for a change in resolution
+			int newWidth = -1;
+			int newHeight = -1;
+			if (m_Texture != null)
+			{
+#if DLL_METHODS
+				newWidth = Native._GetWidth(m_iPlayerIndex);
+				newHeight = Native._GetHeight(m_iPlayerIndex);
+#else
+				newWidth = m_Video.Call<int>("GetWidth");
+				newHeight = m_Video.Call<int>("GetHeight");
+#endif
+				if (newWidth != m_Width || newHeight != m_Height)
+				{
+					m_Texture = null;
+					m_TextureHandle = 0;
+				}
+			}
+#if DLL_METHODS
+			int textureHandle = Native._GetTextureHandle(m_iPlayerIndex);
+#else
+				int textureHandle = m_Video.Call<int>("GetTextureHandle");
+#endif
+			if (textureHandle != 0 && textureHandle != m_TextureHandle)
+			{
+				// Already got? (from above)
+				if (newWidth == -1 || newHeight == -1)
+				{
+#if DLL_METHODS
+					newWidth = Native._GetWidth(m_iPlayerIndex);
+					newHeight = Native._GetHeight(m_iPlayerIndex);
+#else
+					newWidth = m_Video.Call<int>("GetWidth");
+					newHeight = m_Video.Call<int>("GetHeight");
+#endif
+				}
+
+				if (Mathf.Max(newWidth, newHeight) > SystemInfo.maxTextureSize)
+				{
+					m_Width = newWidth;
+					m_Height = newHeight;
+					m_TextureHandle = textureHandle;
+					Debug.LogError("[AVProVideo] Video dimensions larger than maxTextureSize");
+				}
+				else if (newWidth > 0 && newHeight > 0)
+				{
+					m_Width = newWidth;
+					m_Height = newHeight;
+					m_TextureHandle = textureHandle;
+
+					switch (m_API)
+					{
+						case Android.VideoApi.MediaPlayer:
+							_playerDescription = "MediaPlayer";
+							break;
+						case Android.VideoApi.ExoPlayer:
+							_playerDescription = "ExoPlayer";
+							break;
+						default:
+							_playerDescription = "UnknownPlayer";
+							break;
+					}
+
+					if (m_UseFastOesPath)
+					{
+						_playerDescription += " OES";
+					}
+					else
+					{
+						_playerDescription += " NonOES";
+					}
+
+					Helper.LogInfo("Using playback path: " + _playerDescription + " (" + m_Width + "x" + m_Height + "@" + GetVideoFrameRate().ToString("F2") + ")");
+
+					// NOTE: From Unity 5.4.x when using OES textures, an error "OPENGL NATIVE PLUG-IN ERROR: GL_INVALID_OPERATION: Operation illegal in current state" will be logged.
+					// We assume this is because we're passing in TextureFormat.RGBA32 which isn't the true texture format.  This error should be safe to ignore.
+					m_Texture = Texture2D.CreateExternalTexture(m_Width, m_Height, TextureFormat.RGBA32, false, false, new System.IntPtr(textureHandle));
+					if (m_Texture != null)
+					{
+						ApplyTextureProperties(m_Texture);
+					}
+					Helper.LogInfo("Texture ID: " + textureHandle);
+				}
+			}
+
+#if AVPROVIDEO_FIXREGRESSION_TEXTUREQUALITY_UNITY542
+	#if UNITY_2022_2_OR_NEWER
+			// Note: See https://docs.unity3d.com/2022.2/Documentation/ScriptReference/QualitySettings-masterTextureLimit.html
+			int textureQualityToTestAgainst = QualitySettings.globalTextureMipmapLimit;
+	#else
+			int textureQualityToTestAgainst = QualitySettings.masterTextureLimit;
+	#endif
+
+			// In Unity 5.4.2 and above the video texture turns black when changing the TextureQuality in the Quality Settings
+			// The code below gets around this issue.  A bug report has been sent to Unity.  So far we have tested and replicated the
+			// "bug" in Windows only, but a user has reported it in Android too.  
+			// Texture.GetNativeTexturePtr() must sync with the rendering thread, so this is a large performance hit!
+			if (_textureQuality != textureQualityToTestAgainst)
+			{
+				if (m_Texture != null && textureHandle > 0 && m_Texture.GetNativeTexturePtr() == System.IntPtr.Zero)
+				{
+					//Debug.Log("RECREATING");
+					m_Texture.UpdateExternalTexture(new System.IntPtr(textureHandle));
+				}
+
+				_textureQuality = textureQualityToTestAgainst;
+			}
+#endif
+		}
+
+		protected override void ApplyTextureProperties(Texture texture)
+		{
+			// NOTE: According to OES_EGL_image_external: For external textures, the default min filter is GL_LINEAR and the default S and T wrap modes are GL_CLAMP_TO_EDGE
+			// See https://www.khronos.org/registry/gles/extensions/OES/OES_EGL_image_external_essl3.txt
+			// But there is a new extension that allows some wrap modes:
+			// https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_EGL_image_external_wrap_modes.txt
+			// So really we need to detect whether these extensions exist when m_UseFastOesPath is true
+			//if (!m_UseFastOesPath)
+			{
+				base.ApplyTextureProperties(texture);
+			}
+		}
+
+		public override bool PlayerSupportsLinearColorSpace()
+		{
+			return false;
+		}
+
+		public override float[] GetTextureTransform()
+		{
+			float[] transform = null;
+			if (m_Video != null)
+			{
+				transform = m_Video.Call<float[]>("GetTextureTransform");
+				/*if (transform != null)
+				{
+					Debug.Log("xform: " + transform[0] + " " + transform[1] + " " + transform[2] + " " + transform[3] + " " + transform[4] + " " + transform[5]);
+				}*/
+			}
+			return transform;
+		}
+
+		public override void Dispose()
+		{
+			//Debug.LogError("DISPOSE");
+
+			if (m_Video != null)
+			{
+				m_Video.Call("SetDeinitialiseFlagged");
+
+				m_Video.Dispose();
+				m_Video = null;
+			}
+
+			if (s_Interface != null)
+			{
+				s_Interface.Call("DestroyPlayer", m_iPlayerIndex);
+			}
+
+			if (m_Texture != null)
+			{
+				Texture2D.Destroy(m_Texture);
+				m_Texture = null;
+			}
+
+			// Deinitialise player (replaces call directly as GL textures are involved)
+			AndroidMediaPlayer.IssuePluginEvent( Native.AVPPluginEvent.PlayerDestroy, m_iPlayerIndex );
+		}
+
+		private void UpdateTimeRanges()
+		{
+			if( m_Video != null )
+			{
+				// Seekable time ranges
+				AndroidJavaObject seekableReturnObject = m_Video.Call<AndroidJavaObject>("GetSeekableTimeRanges");
+				if( seekableReturnObject.GetRawObject().ToInt32() != 0 )
+				{
+					double[] aSeekableRanges = AndroidJNIHelper.ConvertFromJNIArray<double[]>( seekableReturnObject.GetRawObject() );
+					if( aSeekableRanges.Length > 1 )
+					{
+						int iNumRanges = aSeekableRanges.Length / 2;
+						_seekableTimes._ranges = new TimeRange[ iNumRanges ];
+
+						int iRangeIndex = 0;
+						for( int iRange = 0; iRange < iNumRanges; ++iRange )
+						{
+							_seekableTimes._ranges[ iRange ] = new TimeRange( aSeekableRanges[ iRangeIndex ], aSeekableRanges[ iRangeIndex + 1 ] );
+							iRangeIndex += 2;
+						}
+						_seekableTimes.CalculateRange();
+					}
+
+					seekableReturnObject.Dispose();
+				}
+
+				// Buffered time ranges
+				AndroidJavaObject bufferedReturnObject = m_Video.Call<AndroidJavaObject>("GetBufferedTimeRanges");
+				if( bufferedReturnObject.GetRawObject().ToInt32() != 0 )
+				{
+					double[] aBufferedRanges = AndroidJNIHelper.ConvertFromJNIArray<double[]>( bufferedReturnObject.GetRawObject() );
+					if( aBufferedRanges.Length > 1 )
+					{
+						int iNumRanges = aBufferedRanges.Length / 2;
+						_bufferedTimes._ranges = new TimeRange[ iNumRanges ];
+
+						int iRangeIndex = 0;
+						for( int iRange = 0; iRange < iNumRanges; ++iRange )
+						{
+							_bufferedTimes._ranges[iRange] = new TimeRange( aBufferedRanges[iRangeIndex], aBufferedRanges[iRangeIndex + 1] );
+							iRangeIndex += 2;
+						}
+						_bufferedTimes.CalculateRange();
+					}
+
+					bufferedReturnObject.Dispose();
+				}
+			}
+		}
+
+		bool isMainThread()
+		{
+			return m_MainThread.Equals(System.Threading.Thread.CurrentThread);
+		}
+
+		public override int GetAudioChannelCount()
+		{
+			return Native._GetCurrentAudioTrackNumChannels(m_iPlayerIndex);
+		}
+/*
+		public override AudioChannelMaskFlags GetAudioChannelMask()
+		{
+			return (AudioChannelMaskFlags)Native.GetAudioChannelMask(_instance);
+		}
+*/
+
+		public override int GrabAudio(float[] buffer, int sampleCount, int channelCount)
+		{
+			int iReturn = 0;
+
+			// Get audio data
+			iReturn = Native._GrabAudioNative( buffer, m_iPlayerIndex, sampleCount, channelCount );
+
+			return iReturn;
+		}
+
+		public override int GetAudioBufferedSampleCount()
+		{
+			int iBufferedSampleCount = 0;
+
+			if (m_Video != null)
+			{
+				// Get audio data
+				iBufferedSampleCount = m_Video.Call<int>("GetAudioBufferedSampleCount");
+			}
+
+			return iBufferedSampleCount;
+		}
+
+		internal override bool InternalIsChangedTextCue()
+		{
+			// Has the text cue changed since the last frame 'tick'
+			if( m_Video != null )
+			{
+				return m_Video.Call<bool>("GetTextCueDirty");
+			}
+
+			return false;
+		}
+
+		internal override string InternalGetCurrentTextCue()
+		{
+			// Return a pointer to the current text cue string in UTF16 format
+			if ( m_Video != null )
+			{
+				return m_Video.Call<string>("GetCurrentTextCue");
+			}
+
+			return string.Empty;
+		}
+
+		internal override bool InternalIsChangedTracks(TrackType trackType)
+		{
+			// Has it changed since the last frame 'tick'
+			bool result = false;
+			switch (trackType)
+			{
+				case TrackType.Video:
+					{
+						result = ( m_Video != null ) ? m_Video.Call<bool>("GetVideoTracksDirty") : false;
+						break;
+					}
+				case TrackType.Audio:
+					{
+						result = ( m_Video != null ) ? m_Video.Call<bool>("GetAudioTracksDirty") : false;
+						break;
+					}
+				case TrackType.Text:
+					{
+						result = ( m_Video != null ) ? m_Video.Call<bool>("GetTextTracksDirty") : false;
+						break;
+					}
+			}
+			return result;
+		}
+
+		internal override int InternalGetTrackCount(TrackType trackType)
+		{
+			int result = 0;
+			switch (trackType)
+			{
+				case TrackType.Video:
+					{
+						result = ( m_Video != null ) ? m_Video.Call<int>("GetNumberVideoTracks") : 0;
+						break;
+					}
+				case TrackType.Audio:
+					{
+						result = ( m_Video != null ) ? m_Video.Call<int>("GetNumberAudioTracks") : 0;
+						break;
+					}
+				case TrackType.Text:
+					{
+						result = ( m_Video != null ) ? m_Video.Call<int>("GetNumberTextTracks") : 0;
+						break;
+					}
+			}
+			return result;
+		}
+
+		internal override bool InternalSetActiveTrack(TrackType trackType, int trackUid)
+		{
+			bool result = false;
+			switch (trackType)
+			{
+				case TrackType.Video:
+					{
+						result = ( m_Video != null ) ? m_Video.Call<bool>("SetVideoTrack", trackUid) : false;
+						break;
+					}
+				case TrackType.Audio:
+					{
+						result = ( m_Video != null ) ? m_Video.Call<bool>("SetAudioTrack", trackUid) : false;
+						break;
+					}
+				case TrackType.Text:
+					{
+						result = ( m_Video != null ) ? m_Video.Call<bool>("SetTextTrack", trackUid) : false;
+						break;
+					}
+			}
+			return result;
+		}
+
+		internal override TrackBase InternalGetTrackInfo(TrackType trackType, int trackIndex, ref bool isActiveTrack)
+		{
+			TrackBase result = null;
+			switch (trackType)
+			{
+				case TrackType.Video:
+					{
+						if (m_Video != null)
+						{
+							AndroidJavaObject returnObject = m_Video.Call<AndroidJavaObject>("GetVideoTrackInfo", trackIndex);
+							if (returnObject.GetRawObject().ToInt32() != 0)
+							{
+								String[] aReturn = AndroidJNIHelper.ConvertFromJNIArray<String[]>(returnObject.GetRawObject());
+								bool bReturn = (aReturn.Length > 0) ? (int.Parse(aReturn[0]) == 1) : false;
+
+								if (bReturn)
+								{
+									VideoTrack videoTrack = new VideoTrack(trackIndex, aReturn[1], aReturn[2], (aReturn[3] == "1"));
+
+									int bitrate = 0;
+									bool gotBitrate = Int32.TryParse(aReturn[4], out bitrate);
+									if( gotBitrate )
+									{
+										videoTrack.Bitrate = bitrate;
+									}
+
+									result = videoTrack;
+
+									isActiveTrack = (m_Video != null && m_Video.Call<int>("GetCurrentVideoTrackIndex") == trackIndex);
+								}
+
+								returnObject.Dispose();
+							}
+						}
+					}
+					break;
+
+				case TrackType.Audio:
+					{
+						if (m_Video != null)
+						{
+							AndroidJavaObject returnObject = m_Video.Call<AndroidJavaObject>("GetAudioTrackInfo", trackIndex);
+							if (returnObject.GetRawObject().ToInt32() != 0)
+							{
+								String[] aReturn = AndroidJNIHelper.ConvertFromJNIArray<String[]>( returnObject.GetRawObject() );
+								bool bReturn = ( aReturn.Length > 0 ) ? ( int.Parse( aReturn[ 0 ]) == 1 ) : false;
+
+								if (bReturn)
+								{
+									int iBitrate = 0;
+									int.TryParse( aReturn[ 4 ], out iBitrate );
+
+									int iChannelCount = 0;
+									int.TryParse(aReturn[ 5 ], out iChannelCount);
+
+									result = new AudioTrack( trackIndex, aReturn[ 1 ], aReturn[ 2 ], (aReturn[ 3 ] == "1") );
+
+									isActiveTrack = (m_Video != null && m_Video.Call<int>("GetCurrentAudioTrackIndex") == trackIndex);
+								}
+
+								returnObject.Dispose();
+							}
+						}
+					}
+					break;
+				
+				case TrackType.Text:
+					{
+						if (m_Video != null)
+						{
+							AndroidJavaObject returnObject = m_Video.Call<AndroidJavaObject>( "GetTextTrackInfo", trackIndex );
+							if (returnObject.GetRawObject().ToInt32() != 0)
+							{
+								String[] aReturn = AndroidJNIHelper.ConvertFromJNIArray<String[]>( returnObject.GetRawObject() );
+								bool bReturn = ( aReturn.Length > 0 ) ? ( int.Parse(aReturn[ 0 ] ) == 1 ) : false;
+
+								int uid = -1;
+
+								if( bReturn )
+								{
+									int.TryParse(aReturn[1], out uid);
+
+									result = new TextTrack(uid, aReturn[ 2 ], aReturn[ 3 ], false);
+
+									isActiveTrack = (m_Video != null && m_Video.Call<int>("GetCurrentTextTrackIndex") == trackIndex);
+								}
+
+								returnObject.Dispose();
+							}
+						}
+					}
+					break;
+			}
+			return result;
+		}
+
+		internal /*override*/ int InternalGetAdaptiveStreamCount(TrackType trackType, int trackIndex = -1)
+		{
+			int result = 0;
+			switch (trackType)
+			{
+				case TrackType.Video:
+					{
+						result = (m_Video != null) ? m_Video.Call<int>("GetNumberVideoAdaptiveStreams", trackIndex) : 0;
+
+						Debug.Log("[AVProVideo]: InternalGetAdaptiveStreamCount return = " + result);
+						break;
+					}
+				case TrackType.Audio:
+					{
+						break;
+					}
+				case TrackType.Text:
+					{
+						break;
+					}
+			}
+			return result;
+		}
+
+		internal /*override*/ void InternalGetAdaptiveStreamInfo(TrackType trackType, int trackIndex = -1)
+		{
+			switch( trackType )
+			{
+				case TrackType.Video:
+					{
+						if( m_Video != null )
+						{
+							AndroidJavaObject returnObject = m_Video.Call<AndroidJavaObject>("GetVideoAdaptiveStreamInfo", trackIndex);
+							if( returnObject.GetRawObject().ToInt32() != 0 )
+							{
+								String[] aReturn = AndroidJNIHelper.ConvertFromJNIArray<String[]>(returnObject.GetRawObject());
+								bool bReturn = (aReturn.Length > 0) ? (int.Parse(aReturn[0]) == 1) : false;
+
+								string toLog = "";
+								foreach( string str in aReturn )	{ toLog += str + ", "; }
+								Debug.Log( "[AVProVideo]: InternalGetAdaptiveStreamInfo return = " + toLog );
+
+								if ( bReturn )
+								{
+								}
+
+								returnObject.Dispose();
+							}
+						}
+					}
+					break;
+
+				case TrackType.Audio:
+					{
+					}
+					break;
+
+				case TrackType.Text:
+					{
+					}
+					break;
+			}
+		}
+
+		internal /*override*/ int SetVideoAdaptiveStreamIndex(TrackType trackType, int streamIndex)
+		{
+			int result = 0;
+			switch( trackType )
+			{
+				case TrackType.Video:
+					{
+						Debug.Log("[AVProVideo]: SetVideoAdaptiveStreamIndex : streamIndex = " + streamIndex);
+
+						result = (m_Video != null) ? m_Video.Call<int>("SetVideoAdaptiveStreams", streamIndex) : 0;
+						break;
+					}
+				case TrackType.Audio:
+					{
+						break;
+					}
+				case TrackType.Text:
+					{
+						break;
+					}
+			}
+			return result;
+		}
+
+		private Vector2 GetPreferredVideoResolution(MediaPlayer.OptionsAndroid.Resolution preferredMaximumResolution, Vector2 customPreferredMaximumResolution)
+		{
+			Vector2 vResolution = new Vector2( 0.0f, 0.0f );
+
+			switch( preferredMaximumResolution )
+			{
+				case MediaPlayer.OptionsAndroid.Resolution.NoPreference:
+					break;
+				case MediaPlayer.OptionsAndroid.Resolution._480p:
+					vResolution.x = 640;
+					vResolution.y = 480;
+					break;
+				case MediaPlayer.OptionsAndroid.Resolution._720p:
+					vResolution.x = 1280;
+					vResolution.y = 720;
+					break;
+				case MediaPlayer.OptionsAndroid.Resolution._1080p:
+					vResolution.x = 1920;
+					vResolution.y = 1080;
+					break;
+				case MediaPlayer.OptionsAndroid.Resolution._2160p:
+					vResolution.x = 3840;
+					vResolution.y = 2160;
+					break;
+				case MediaPlayer.OptionsAndroid.Resolution.Custom:
+#if UNITY_2017_2_OR_NEWER
+					vResolution.x = customPreferredMaximumResolution.x;
+					vResolution.y = customPreferredMaximumResolution.y;
+#endif
+					break;
+			}
+
+			return vResolution;
+		}
+
+		public override bool IsMediaCachingSupported()
+		{
+			if( m_Video != null )
+			{
+				return m_Video.Call<bool>("IsMediaCachingSupported");
+			}
+
+			return true;
+		}
+
+		public override void AddMediaToCache(string url, string headers, MediaCachingOptions options)
+		{
+			if (m_Video != null)
+			{
+				double dMinBitrate = -1.0f;
+				int iMinWidth = -1;
+				int iMinHeight = -1;
+
+				double dMaxBitrate = -1.0f;
+				int iMaxWidth = -1;
+				int iMaxHeight = -1;
+				
+				if (options != null )
+				{
+					dMinBitrate = options.minimumRequiredBitRate;
+					iMinWidth = (int)( options.minimumRequiredResolution.x );
+					iMinHeight = (int)( options.minimumRequiredResolution.y );
+
+					dMaxBitrate = options.maximumRequiredBitRate;
+					iMaxWidth = (int)(options.maximumRequiredResolution.x);
+					iMaxHeight = (int)(options.maximumRequiredResolution.y);
+				}
+				m_Video.Call("AddMediaToCache", url, headers, dMinBitrate, iMinWidth, iMinHeight, dMaxBitrate, iMaxWidth, iMaxHeight);
+			}
+		}
+
+		public override void RemoveMediaFromCache(string url)
+		{
+			if(m_Video != null)
+			{
+				m_Video.Call("RemoveMediaFromCache", url);
+			}
+		}
+
+		public override void CancelDownloadOfMediaToCache(string url)
+		{
+			if (m_Video != null)
+			{
+				m_Video.Call("CancelDownloadOfMediaToCache", url);
+			}
+		}
+
+		public override void PauseDownloadOfMediaToCache(string url)
+		{
+			if (m_Video != null)
+			{
+				m_Video.Call("PauseDownloadOfMediaToCache", url);
+			}
+		}
+
+		public override void ResumeDownloadOfMediaToCache(string url)
+		{
+			if (m_Video != null)
+			{
+				m_Video.Call("ResumeDownloadOfMediaToCache", url);
+			}
+		}
+
+		public override CachedMediaStatus GetCachedMediaStatus(string url, ref float progress)
+		{
+			CachedMediaStatus eStatus = CachedMediaStatus.NotCached;
+
+			if (m_Video != null)
+			{
+				float[] afReturn = m_Video.Call<float[]>("GetCachedMediaStatus", url);
+				eStatus = (CachedMediaStatus)( afReturn[ 0 ] );
+				progress = afReturn[ 1 ] * 0.01f;
+
+//				if( eStatus != CachedMediaStatus.NotCached && progress < 1.0f )
+//				{
+//					Debug.Log("AVProVideo: GetCachedMediaStatus | url = " + url + " | eStatus = " + eStatus + " | progress = " + progress);
+//				}
+			}
+
+			return eStatus;
+		}
+
+
+		private struct Native
+		{
+			internal const string LibraryName = "AVProVideo2Native";
+
+			[DllImport (LibraryName)]
+			public static extern IntPtr GetRenderEventFunc();
+
+			[DllImport(LibraryName)]
+			public static extern bool JNIAttachCurrentThread();
+
+			[DllImport(LibraryName)]
+			public static extern void JNIDetachCurrentThread();
+
+			[DllImport(LibraryName)]
+			public static extern int _GetCurrentAudioTrackNumChannels( int iPlayerIndex );
+
+			[DllImport(LibraryName)]
+//			unsafe public static extern int _GrabAudioNative(float* pBuffer, int iPlayerIndex, int sampleCount, int channelCount);
+			public static extern int _GrabAudioNative(float[] pBuffer, int iPlayerIndex, int sampleCount, int channelCount);
+
+			[DllImport (LibraryName)]
+			public static extern int _GetWidth( int iPlayerIndex );
+
+			[DllImport (LibraryName)]
+			public static extern int _GetHeight( int iPlayerIndex );
+			
+			[DllImport (LibraryName)]
+			public static extern int _GetTextureHandle( int iPlayerIndex );
+
+			[DllImport (LibraryName)]
+			public static extern double _GetDuration( int iPlayerIndex );
+
+			[DllImport (LibraryName)]
+			public static extern int _GetLastErrorCode( int iPlayerIndex );
+
+			[DllImport (LibraryName)]
+			public static extern int _GetFrameCount( int iPlayerIndex );
+		
+			[DllImport (LibraryName)]
+			public static extern float _GetVideoDisplayRate( int iPlayerIndex );
+
+			[DllImport (LibraryName)]
+			public static extern bool _CanPlay( int iPlayerIndex );
+			
+			public enum AVPPluginEvent
+			{
+				Nop,
+				PlayerSetup,
+				PlayerUpdate,
+				PlayerDestroy,
+				ExtractFrame,
+			}
+		}
+	}
+}
+#endif
+	  

+ 12 - 0
Assets/AVProVideo/Runtime/Scripts/Internal/Players/AndroidMediaPlayer.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 80eb525dd677aa440823910b09b23ae0
+timeCreated: 1438698292
+licenseType: Pro
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 439 - 0
Assets/AVProVideo/Runtime/Scripts/Internal/Players/AppleMediaPlayer+Native.cs

@@ -0,0 +1,439 @@
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+#if UNITY_2017_2_OR_NEWER && (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || (!UNITY_EDITOR && (UNITY_IOS || UNITY_TVOS)))
+
+using System;
+using System.Runtime.InteropServices;
+using UnityEngine;
+
+namespace RenderHeads.Media.AVProVideo
+{
+	public sealed partial class AppleMediaPlayer
+	{
+		internal partial struct Native
+		{
+#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
+			private const string PluginName = "AVProVideo";
+#elif UNITY_IOS || UNITY_TVOS
+			private const string PluginName = "__Internal";
+#endif
+
+			// Video settings
+
+			internal enum AVPPlayerVideoPixelFormat: int
+			{
+				Invalid,
+				Bgra,
+				YCbCr420
+			}
+
+			[Flags]
+			internal enum AVPPlayerVideoOutputSettingsFlags: int
+			{
+				None             = 0,
+				LinearColorSpace = 1 << 0,
+				GenerateMipmaps  = 1 << 1,
+			}
+
+			// Audio settings
+
+			internal enum AVPPlayerAudioOutputMode : int
+			{
+				SystemDirect,
+				Unity,
+				SystemDirectWithCapture,
+			}
+
+			// Network settings
+
+			[Flags]
+			internal enum AVPPlayerNetworkSettingsFlags: int
+			{
+				None                 = 0,
+				PlayWithoutBuffering = 1 << 0,
+				UseSinglePlayerItem  = 1 << 1,
+			}
+
+			[StructLayout(LayoutKind.Sequential)]
+			internal struct AVPPlayerSettings
+			{
+				// Video
+				internal AVPPlayerVideoPixelFormat pixelFormat;
+				internal AVPPlayerVideoOutputSettingsFlags videoFlags;
+				internal float preferredMaximumResolution_width;
+				internal float preferredMaximumResolution_height;
+				internal float maximumPlaybackRate;
+
+				// Audio
+				internal AVPPlayerAudioOutputMode audioOutputMode;
+				internal int sampleRate;
+				internal int bufferLength;
+				internal int audioFlags;
+
+				// Network
+				internal double preferredPeakBitRate;
+				internal double preferredForwardBufferDuration;
+				internal AVPPlayerNetworkSettingsFlags networkFlags;
+			}
+
+			[Flags]
+			internal enum AVPPlayerStatus : int
+			{
+				Unknown                   = 0,
+				ReadyToPlay               = 1 <<  0,
+				Playing                   = 1 <<  1,
+				Paused                    = 1 <<  2,
+				Finished                  = 1 <<  3,
+				Seeking                   = 1 <<  4,
+				Buffering                 = 1 <<  5,
+				Stalled                   = 1 <<  6,
+				ExternalPlaybackActive    = 1 <<  7,
+				Cached                    = 1 <<  8,
+				FinishedSeeking           = 1 <<  9,
+
+				UpdatedAssetInfo          = 1 << 16,
+				UpdatedTexture            = 1 << 17,
+				UpdatedBufferedTimeRanges = 1 << 18,
+				UpdatedSeekableTimeRanges = 1 << 19,
+				UpdatedText               = 1 << 20,
+
+				HasVideo                  = 1 << 24,
+				HasAudio                  = 1 << 25,
+				HasText                   = 1 << 26,
+				HasMetadata               = 1 << 27,
+
+				Failed                    = 1 << 31
+			}
+
+			[Flags]
+			internal enum AVPPlayerFlags : int
+			{
+				None                  = 0,
+				Looping               = 1 <<  0,
+				Muted                 = 1 <<  1,
+				AllowExternalPlayback = 1 <<  2,
+				ResumePlayback        = 1 << 16,	// iOS only, resumes playback after audio session route change
+				Dirty                 = 1 << 31
+			}
+
+			internal enum AVPPlayerExternalPlaybackVideoGravity : int
+			{
+				Resize,
+				ResizeAspect,
+				ResizeAspectFill
+			};
+
+			[StructLayout(LayoutKind.Sequential)]
+			internal struct AVPPlayerSize
+			{
+				internal float width;
+				internal float height;
+			}
+
+			[StructLayout(LayoutKind.Sequential)]
+			internal struct AVPAffineTransform
+			{
+				internal float a;
+				internal float b;
+				internal float c;
+				internal float d;
+				internal float tx;
+				internal float ty;
+			}
+
+			[Flags]
+			internal enum AVPPlayerAssetFlags : int
+			{
+				None                  = 0,
+				CompatibleWithAirPlay = 1 << 0,
+			};
+
+			[StructLayout(LayoutKind.Sequential)]
+			internal struct AVPPlayerAssetInfo
+			{
+				internal double duration;
+				internal AVPPlayerSize dimensions;
+				internal float frameRate;
+				internal int videoTrackCount;
+				internal int audioTrackCount;
+				internal int textTrackCount;
+				internal AVPPlayerAssetFlags flags;
+			}
+
+			[Flags]
+			internal enum AVPPlayerTrackFlags: int
+			{
+				Default = 1 << 0,
+			}
+
+			internal enum AVPPlayerVideoTrackStereoMode: int
+			{
+				Unknown,
+				Monoscopic,
+				StereoscopicTopBottom,
+				StereoscopicLeftRight,
+				StereoscopicCustom,
+				StereoscopicRightLeft,
+			}
+
+			[Flags]
+			internal enum AVPPlayerVideoTrackFlags: int
+			{
+				HasAlpha = 1 << 0,
+			}
+
+			[StructLayout(LayoutKind.Sequential)]
+			internal struct AVPPlayerVideoTrackInfo
+			{
+				[MarshalAs(UnmanagedType.LPWStr)] internal string name;
+				[MarshalAs(UnmanagedType.LPWStr)] internal string language;
+				internal int trackId;
+				internal float estimatedDataRate;
+				internal uint codecSubtype;
+				internal AVPPlayerTrackFlags flags;
+
+				internal AVPPlayerSize dimensions;
+				internal float frameRate;
+				internal AVPAffineTransform transform;
+				internal AVPPlayerVideoTrackStereoMode stereoMode;
+				internal int bitsPerComponent;
+				internal AVPPlayerVideoTrackFlags videoTrackFlags;
+
+				internal Matrix4x4 yCbCrTransform;
+			}
+
+			[StructLayout(LayoutKind.Sequential)]
+			internal struct AVPPlayerAudioTrackInfo
+			{
+				[MarshalAs(UnmanagedType.LPWStr)] internal string name;
+				[MarshalAs(UnmanagedType.LPWStr)] internal string language;
+				internal int trackId;
+				internal float estimatedDataRate;
+				internal uint codecSubtype;
+				internal AVPPlayerTrackFlags flags;
+
+				internal double sampleRate;
+				internal uint channelCount;
+				internal uint channelLayoutTag;
+				internal AudioChannelMaskFlags channelBitmap;
+			}
+
+			[StructLayout(LayoutKind.Sequential)]
+			internal struct AVPPlayerTextTrackInfo
+			{
+				[MarshalAs(UnmanagedType.LPWStr)] internal string name;
+				[MarshalAs(UnmanagedType.LPWStr)] internal string language;
+				internal int trackId;
+				internal float estimatedDataRate;
+				internal uint codecSubtype;
+				internal AVPPlayerTrackFlags flags;
+			}
+
+			[StructLayout(LayoutKind.Sequential)]
+			internal struct AVPPlayerTimeRange
+			{
+				internal double start;
+				internal double duration;
+			};
+
+			[StructLayout(LayoutKind.Sequential)]
+			internal struct AVPPlayerState
+			{
+				internal AVPPlayerStatus status;
+				internal double currentTime;
+				internal double currentDate;
+				internal int selectedVideoTrack;
+				internal int selectedAudioTrack;
+				internal int selectedTextTrack;
+				internal int bufferedTimeRangesCount;
+				internal int seekableTimeRangesCount;
+				internal int audioCaptureBufferedSamplesCount;
+			}
+
+			internal enum AVPPlayerTextureFormat: int
+			{
+				Unknown,
+				BGRA8,
+				R8,
+				RG8,
+				BC1,
+				BC3,
+				BC4,
+				BC5,
+				BC7,
+				BGR10A2,
+				R16,
+				RG16,
+				BGR10XR,
+			}
+
+			[StructLayout(LayoutKind.Sequential)]
+			internal struct AVPPlayerTexturePlane
+			{
+				internal IntPtr plane;
+				internal int width;
+				internal int height;
+				internal AVPPlayerTextureFormat textureFormat;
+			}
+
+			[Flags]
+			internal enum AVPPlayerTextureFlags: int
+			{
+				None      = 0,
+				Flipped   = 1 << 0,
+				Linear    = 1 << 1,
+				Mipmapped = 1 << 2,
+			}
+
+			internal enum AVPPlayerTextureYCbCrMatrix: int
+			{
+				Identity,
+				ITU_R_601,
+				ITU_R_709,
+			}
+
+			[StructLayout(LayoutKind.Sequential)]
+			internal struct AVPPlayerTexture
+			{
+				[MarshalAs(UnmanagedType.ByValArray, SizeConst=2)]
+				internal AVPPlayerTexturePlane[] planes;
+				internal long itemTime;
+				internal int frameCount;
+				internal int planeCount;
+				internal AVPPlayerTextureFlags flags;
+				internal AVPPlayerTextureYCbCrMatrix YCbCrMatrix;
+			};
+
+			[StructLayout(LayoutKind.Sequential)]
+			internal struct AVPPlayerText
+			{
+				internal IntPtr buffer;
+				internal long itemTime;
+				internal int length;
+				internal int sequence;
+			};
+
+			internal enum AVPPlayerTrackType: int
+			{
+				Video,
+				Audio,
+				Text
+			};
+
+#if !UNITY_EDITOR && (UNITY_IOS || UNITY_TVOS)
+			[DllImport(PluginName)]
+			internal static extern void AVPPluginBootstrap();
+#endif
+
+			[DllImport(PluginName)]
+			private static extern IntPtr AVPPluginGetVersionStringPointer();
+
+			internal static string GetPluginVersion()
+			{
+				return System.Runtime.InteropServices.Marshal.PtrToStringAnsi(AVPPluginGetVersionStringPointer());
+			}
+
+			[DllImport(PluginName)]
+			internal static extern IntPtr AVPPluginMakePlayer(Native.AVPPlayerSettings settings);
+
+			[DllImport(PluginName)]
+			internal static extern IntPtr AVPPlayerRelease(IntPtr player);
+
+			[DllImport(PluginName)]
+			internal static extern void AVPPlayerGetState(IntPtr player, ref AVPPlayerState state);
+
+			[DllImport(PluginName)]
+			internal static extern void AVPPlayerSetFlags(IntPtr player, int flags);
+
+			[DllImport(PluginName)]
+			internal static extern void AVPPlayerGetAssetInfo(IntPtr player, ref AVPPlayerAssetInfo info);
+
+			[DllImport(PluginName)]
+			internal static extern void AVPPlayerGetVideoTrackInfo(IntPtr player, int index, ref AVPPlayerVideoTrackInfo info);
+
+			[DllImport(PluginName)]
+			internal static extern void AVPPlayerGetAudioTrackInfo(IntPtr player, int index, ref AVPPlayerAudioTrackInfo info);
+
+			[DllImport(PluginName)]
+			internal static extern void AVPPlayerGetTextTrackInfo(IntPtr player, int index, ref AVPPlayerTextTrackInfo info);
+
+			[DllImport(PluginName)]
+			internal static extern void AVPPlayerGetBufferedTimeRanges(IntPtr player, AVPPlayerTimeRange[] ranges, int count);
+
+			[DllImport(PluginName)]
+			internal static extern void AVPPlayerGetSeekableTimeRanges(IntPtr player, AVPPlayerTimeRange[] ranges, int count);
+
+			[DllImport(PluginName)]
+			internal static extern void AVPPlayerGetTexture(IntPtr player, ref AVPPlayerTexture texture);
+
+			[DllImport(PluginName)]
+			internal static extern void AVPPlayerGetText(IntPtr player, ref AVPPlayerText text);
+
+			[DllImport(PluginName)]
+			internal static extern void AVPPlayerSetPlayerSettings(IntPtr player, AVPPlayerSettings settings);
+			
+			[DllImport(PluginName)]
+			[return: MarshalAs(UnmanagedType.U1)]
+			internal static extern bool AVPPlayerOpenURL(IntPtr player, string url, string headers);
+
+			[DllImport(PluginName)]
+			internal static extern void AVPPlayerClose(IntPtr player);
+
+			[DllImport(PluginName)]
+			internal static extern int AVPPlayerGetAudio(IntPtr player, float[] buffer, int length);
+
+			[DllImport(PluginName)]
+			internal static extern void AVPPlayerSetRate(IntPtr player, float rate);
+
+			[DllImport(PluginName)]
+			internal static extern void AVPPlayerSetVolume(IntPtr player, float volume);
+
+			[DllImport(PluginName)]
+			internal static extern void AVPPlayerSetExternalPlaybackVideoGravity(IntPtr player, AVPPlayerExternalPlaybackVideoGravity gravity);
+
+			[DllImport(PluginName)]
+			internal static extern void AVPPlayerSeek(IntPtr player, double toTime, double toleranceBefore, double toleranceAfter);
+
+			[DllImport(PluginName)]
+			internal static extern void AVPPlayerSetKeyServerAuthToken(IntPtr player, string token);
+
+			[DllImport(PluginName)]
+			internal static extern void AVPPlayerSetKeyServerURL(IntPtr player, string url);
+
+			[DllImport(PluginName)]
+			internal static extern void AVPPlayerSetDecryptionKey(IntPtr player, byte[] key, int length);
+
+			[DllImport(PluginName)]
+			[return: MarshalAs(UnmanagedType.I1)]
+			internal static extern bool AVPPlayerSetTrack(IntPtr player, AVPPlayerTrackType type, int index);
+
+#if !UNITY_EDITOR && UNITY_IOS
+			public struct MediaCachingOptions
+			{
+				public double minimumRequiredBitRate;
+				public float  minimumRequiredResolution_width;
+				public float  minimumRequiredResolution_height;
+				public string title;
+				public IntPtr artwork;
+				public int    artworkLength;
+			}
+
+			[DllImport(PluginName)]
+			public static extern void AVPPluginCacheMediaForURL(string url, string headers, MediaCachingOptions options);
+
+			[DllImport(PluginName)]
+			public static extern void AVPPluginCancelDownloadOfMediaForURL(string url);
+
+			[DllImport(PluginName)]
+			public static extern void AVPPluginRemoveCachedMediaForURL(string url);
+
+			[DllImport(PluginName)]
+			public static extern int AVPPluginGetCachedMediaStatusForURL(string url, ref float progress);
+#endif
+		}
+	}
+}
+
+#endif

+ 11 - 0
Assets/AVProVideo/Runtime/Scripts/Internal/Players/AppleMediaPlayer+Native.cs.meta

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

+ 1069 - 0
Assets/AVProVideo/Runtime/Scripts/Internal/Players/AppleMediaPlayer.cs

@@ -0,0 +1,1069 @@
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+#if UNITY_2017_2_OR_NEWER && (UNITY_EDITOR_OSX || (!UNITY_EDITOR && (UNITY_STANDALONE_OSX || UNITY_IOS || UNITY_TVOS)))
+
+using System;
+using System.Runtime.InteropServices;
+using System.Text.RegularExpressions;
+using UnityEngine;
+
+namespace RenderHeads.Media.AVProVideo
+{
+	public sealed partial class AppleMediaPlayer : BaseMediaPlayer
+	{
+		private static Regex RxSupportedSchema = new Regex("^(https?|file)://", RegexOptions.None);
+		private static DateTime Epoch = new DateTime(2001, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+
+		static AppleMediaPlayer()
+		{
+			#if !UNITY_EDITOR && (UNITY_IOS || UNITY_TVOS)
+				Native.AVPPluginBootstrap();
+			#endif
+		}
+
+		private IntPtr _player;
+		Native.AVPPlayerSettings _playerSettings;
+
+		private MediaPlayer.OptionsApple _options;
+
+		public AppleMediaPlayer(MediaPlayer.OptionsApple options)
+		{
+			// Keep a handle on the options
+			_options = options;
+
+			// Alert the user to OpenGL renderer being used
+			if (SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.OpenGLCore)
+			{
+				Debug.LogWarning("[AVProVideo] OpenGL is not supported.");
+				Debug.Log("[AVProVideo] The video will play but no video frames will be displayed. Please switch to using the Metal rendering API.");
+			}
+
+			// Configure the video output settings
+			_playerSettings = new Native.AVPPlayerSettings();
+
+			switch (options.textureFormat)
+			{
+				case MediaPlayer.OptionsApple.TextureFormat.BGRA:
+				default:
+					_playerSettings.pixelFormat = Native.AVPPlayerVideoPixelFormat.Bgra;
+					break;
+				case MediaPlayer.OptionsApple.TextureFormat.YCbCr420:
+					_playerSettings.pixelFormat = Native.AVPPlayerVideoPixelFormat.YCbCr420;
+					break;
+			}
+
+			if (options.flags.GenerateMipmaps())
+				_playerSettings.videoFlags |= Native.AVPPlayerVideoOutputSettingsFlags.GenerateMipmaps;
+			if (QualitySettings.activeColorSpace == ColorSpace.Linear)
+				_playerSettings.videoFlags |= Native.AVPPlayerVideoOutputSettingsFlags.LinearColorSpace;
+
+			GetWidthHeightFromResolution(
+				options.preferredMaximumResolution,
+				options.customPreferredMaximumResolution,
+				out _playerSettings.preferredMaximumResolution_width,
+				out _playerSettings.preferredMaximumResolution_height
+			);
+
+			_playerSettings.maximumPlaybackRate = options.maximumPlaybackRate;
+
+			// Configure the audio output settings
+			_playerSettings.audioOutputMode = (Native.AVPPlayerAudioOutputMode)options.audioMode;
+			if (options.audioMode == MediaPlayer.OptionsApple.AudioMode.Unity)
+			{
+				_playerSettings.sampleRate = AudioSettings.outputSampleRate;
+				int numBuffers;
+				AudioSettings.GetDSPBufferSize(out _playerSettings.bufferLength, out numBuffers);
+			}
+
+			// Configure any network settings
+			_playerSettings.preferredPeakBitRate = options.GetPreferredPeakBitRateInBitsPerSecond();
+			_playerSettings.preferredForwardBufferDuration = options.preferredForwardBufferDuration;
+			if (options.flags.PlayWithoutBuffering())
+				_playerSettings.networkFlags |= Native.AVPPlayerNetworkSettingsFlags.PlayWithoutBuffering;
+			if (options.flags.UseSinglePlayerItem())
+				_playerSettings.networkFlags |= Native.AVPPlayerNetworkSettingsFlags.UseSinglePlayerItem;
+
+			// Make the player
+			_player = Native.AVPPluginMakePlayer(_playerSettings);
+
+			// Setup any other flags from the options
+			_flags = _flags.SetAllowExternalPlayback(options.flags.AllowExternalPlayback());
+			_flags = _flags.SetResumePlayback(options.flags.ResumePlaybackAfterAudioSessionRouteChange());
+
+			// Force an update to get our state in sync with the native
+			Update();
+		}
+
+		private static void GetWidthHeightFromResolution(MediaPlayer.OptionsApple.Resolution resolution, Vector2Int custom, out float width, out float height)
+		{
+			switch (resolution)
+			{
+				case MediaPlayer.OptionsApple.Resolution.NoPreference:
+				default:
+					width = 0;
+					height = 0;
+					break;
+				case MediaPlayer.OptionsApple.Resolution._480p:
+					width = 640;
+					height = 480;
+					break;
+				case MediaPlayer.OptionsApple.Resolution._720p:
+					width = 1280;
+					height = 720;
+					break;
+				case MediaPlayer.OptionsApple.Resolution._1080p:
+					width = 1920;
+					height = 1080;
+					break;
+				case MediaPlayer.OptionsApple.Resolution._1440p:
+					width = 2560;
+					height = 1440;
+					break;
+				case MediaPlayer.OptionsApple.Resolution._2160p:
+					width = 3840;
+					height = 2160;
+					break;
+				case MediaPlayer.OptionsApple.Resolution.Custom:
+					width = custom.x;
+					height = custom.y;
+					break;
+			}
+		}
+	}
+
+	// IMediaPlayer
+	public sealed partial class AppleMediaPlayer
+	{
+		private const int MaxTexturePlanes = 2;
+		private Native.AVPPlayerState _state = new Native.AVPPlayerState();
+		private Native.AVPPlayerFlags _flags = Native.AVPPlayerFlags.None;
+		private Native.AVPPlayerAssetInfo _assetInfo = new Native.AVPPlayerAssetInfo();
+		private Native.AVPPlayerVideoTrackInfo[] _videoTrackInfo = new Native.AVPPlayerVideoTrackInfo[0];
+		private Native.AVPPlayerAudioTrackInfo[] _audioTrackInfo = new Native.AVPPlayerAudioTrackInfo[0];
+		private Native.AVPPlayerTextTrackInfo[] _textTrackInfo = new Native.AVPPlayerTextTrackInfo[0];
+		private Native.AVPPlayerTexture _playerTexture;
+		private Native.AVPPlayerText _playerText;
+		private Texture2D[] _texturePlanes = new Texture2D[MaxTexturePlanes];
+		private float _volume = 1.0f;
+		private float _rate = 1.0f;
+
+		public override void OnEnable()
+		{
+
+		}
+
+		public override void Update()
+		{
+			Native.AVPPlayerStatus prevStatus = _state.status;
+			Native.AVPPlayerGetState(_player, ref _state);
+
+			Native.AVPPlayerStatus changedStatus = prevStatus ^ _state.status;
+
+			// Need to make sure that lastError is set when status is failed so that the Error event is triggered
+			if (/*BaseMediaPlayer.*/_lastError == ErrorCode.None && changedStatus.HasFailed() && _state.status.HasFailed())
+			{
+				/*BaseMediaPlayer.*/_lastError = ErrorCode.LoadFailed;
+			}
+
+			if (_state.status.HasUpdatedAssetInfo())
+			{
+				Native.AVPPlayerGetAssetInfo(_player, ref _assetInfo);
+
+				if (_state.status.HasVideo())
+				{
+					_videoTrackInfo = new Native.AVPPlayerVideoTrackInfo[_assetInfo.videoTrackCount];
+					for (int i = 0; i < _assetInfo.videoTrackCount; ++i)
+					{
+						_videoTrackInfo[i] = new Native.AVPPlayerVideoTrackInfo();
+						Native.AVPPlayerGetVideoTrackInfo(_player, i, ref _videoTrackInfo[i]);
+					}
+				}
+
+				if (_state.status.HasAudio())
+				{
+					_audioTrackInfo = new Native.AVPPlayerAudioTrackInfo[_assetInfo.audioTrackCount];
+					for (int i = 0; i < _assetInfo.audioTrackCount; ++i)
+					{
+						_audioTrackInfo[i] = new Native.AVPPlayerAudioTrackInfo();
+						Native.AVPPlayerGetAudioTrackInfo(_player, i, ref _audioTrackInfo[i]);
+					}
+				}
+
+				if (_state.status.HasText())
+				{
+					_textTrackInfo = new Native.AVPPlayerTextTrackInfo[_assetInfo.textTrackCount];
+					for (int i = 0; i < _assetInfo.textTrackCount; ++i)
+					{
+						_textTrackInfo[i] = new Native.AVPPlayerTextTrackInfo();
+						Native.AVPPlayerGetTextTrackInfo(_player, i, ref _textTrackInfo[i]);
+					}
+				}
+
+				/*BaseMediaPlayer.*/UpdateTracks();
+			}
+
+			if (_state.status.HasUpdatedBufferedTimeRanges())
+			{
+				if (_state.bufferedTimeRangesCount > 0)
+				{
+					Native.AVPPlayerTimeRange[] timeRanges = new Native.AVPPlayerTimeRange[_state.bufferedTimeRangesCount];
+					Native.AVPPlayerGetBufferedTimeRanges(_player, timeRanges, timeRanges.Length);
+					_bufferedTimes = ConvertNativeTimeRangesToTimeRanges(timeRanges);
+				}
+				else
+				{
+					_bufferedTimes = new TimeRanges();
+				}
+			}
+
+			if (_state.status.HasUpdatedSeekableTimeRanges())
+			{
+				if (_state.seekableTimeRangesCount > 0)
+				{
+					Native.AVPPlayerTimeRange[] timeRanges = new Native.AVPPlayerTimeRange[_state.seekableTimeRangesCount];
+					Native.AVPPlayerGetSeekableTimeRanges(_player, timeRanges, timeRanges.Length);
+					_seekableTimes = ConvertNativeTimeRangesToTimeRanges(timeRanges);
+				}
+				else
+				{
+					_seekableTimes = new TimeRanges();
+				}
+			}
+
+			if (_state.status.HasUpdatedTexture())
+			{
+				Native.AVPPlayerGetTexture(_player, ref _playerTexture);
+				for (int i = 0; i < _playerTexture.planeCount; ++i)
+				{
+					TextureFormat textureFormat = TextureFormat.BGRA32;
+					switch (_playerTexture.planes[i].textureFormat)
+					{
+						case Native.AVPPlayerTextureFormat.R8:
+							textureFormat = TextureFormat.R8;
+							break;
+						case Native.AVPPlayerTextureFormat.RG8:
+							textureFormat = TextureFormat.RG16;
+							break;
+						case Native.AVPPlayerTextureFormat.BC1:
+							textureFormat = TextureFormat.DXT1;
+							break;
+						case Native.AVPPlayerTextureFormat.BC3:
+							textureFormat = TextureFormat.DXT5;
+							break;
+						case Native.AVPPlayerTextureFormat.BC4:
+							textureFormat = TextureFormat.BC4;
+							break;
+						case Native.AVPPlayerTextureFormat.BC5:
+							textureFormat = TextureFormat.BC5;
+							break;
+						case Native.AVPPlayerTextureFormat.BC7:
+							textureFormat = TextureFormat.BC7;
+							break;
+						case Native.AVPPlayerTextureFormat.BGRA8:
+						default:
+							break;
+					}
+
+					if (_texturePlanes[i] == null ||
+						_texturePlanes[i].width != _playerTexture.planes[i].width ||
+						_texturePlanes[i].height != _playerTexture.planes[i].height ||
+						_texturePlanes[i].format != textureFormat)
+					{
+						// Ensure any existing texture is released.
+						if (_texturePlanes[i] != null)
+						{
+							_texturePlanes[i].UpdateExternalTexture(IntPtr.Zero);
+							_texturePlanes[i] = null;
+						}
+						_texturePlanes[i] = Texture2D.CreateExternalTexture(
+							_playerTexture.planes[i].width,
+							_playerTexture.planes[i].height,
+							textureFormat,
+							_playerTexture.flags.IsMipmapped(),
+							_playerTexture.flags.IsLinear(),
+							_playerTexture.planes[i].plane
+						);
+						base.ApplyTextureProperties(_texturePlanes[i]);
+					}
+					else
+					{
+						_texturePlanes[i].UpdateExternalTexture(_playerTexture.planes[i].plane);
+					}
+				}
+			}
+
+			if (_state.status.HasUpdatedText())
+			{
+				Native.AVPPlayerGetText(_player, ref _playerText);
+				/*BaseMediaPlayer.*/UpdateTextCue();
+			}
+
+			if (_flags.IsDirty())
+			{
+				_flags = _flags.SetDirty(false);
+				Native.AVPPlayerSetFlags(_player, (int)_flags);
+			}
+
+			if (_options.HasChanged())
+			{
+				if (_options.HasChanged(MediaPlayer.OptionsApple.ChangeFlags.PreferredPeakBitRate))
+				{
+					_playerSettings.preferredPeakBitRate = _options.GetPreferredPeakBitRateInBitsPerSecond();
+				}
+				
+				if (_options.HasChanged(MediaPlayer.OptionsApple.ChangeFlags.PreferredForwardBufferDuration))
+				{
+					_playerSettings.preferredForwardBufferDuration = _options.preferredForwardBufferDuration;
+				}
+				
+				if (_options.HasChanged(MediaPlayer.OptionsApple.ChangeFlags.PlayWithoutBuffering))
+				{
+					bool enabled = (_options.flags & MediaPlayer.OptionsApple.Flags.PlayWithoutBuffering) == MediaPlayer.OptionsApple.Flags.PlayWithoutBuffering;
+					_playerSettings.networkFlags = enabled ? _playerSettings.networkFlags |  Native.AVPPlayerNetworkSettingsFlags.PlayWithoutBuffering
+														   : _playerSettings.networkFlags & ~Native.AVPPlayerNetworkSettingsFlags.PlayWithoutBuffering;
+				}
+				
+				if (_options.HasChanged(MediaPlayer.OptionsApple.ChangeFlags.PreferredMaximumResolution))
+				{
+					GetWidthHeightFromResolution(
+						_options.preferredMaximumResolution,
+						_options.customPreferredMaximumResolution,
+						out _playerSettings.preferredMaximumResolution_width,
+						out _playerSettings.preferredMaximumResolution_height);
+				}
+				
+				if (_options.HasChanged(MediaPlayer.OptionsApple.ChangeFlags.AudioMode))
+				{
+					if (_state.status.IsReadyToPlay() == false)
+					{
+						_playerSettings.audioOutputMode = (Native.AVPPlayerAudioOutputMode)_options.audioMode;
+					}
+					else
+					{
+						Debug.LogWarning("[AVProVideo] Unable to change audio mode after media has been loaded and is ready to play");
+						_options.audioMode = _options.previousAudioMode;
+					}
+				}
+
+				Native.AVPPlayerSetPlayerSettings(_player, _playerSettings);
+				
+				_options.ClearChanges();
+			}
+
+			/*BaseMediaPlayer.*/UpdateDisplayFrameRate();
+			/*BaseMediaPlayer.*/UpdateSubtitles();
+		}
+
+		public override void Render()
+		{
+
+		}
+
+		public override IntPtr GetNativePlayerHandle()
+		{
+			return _player;
+		}
+
+		private static TimeRanges ConvertNativeTimeRangesToTimeRanges(Native.AVPPlayerTimeRange[] ranges)
+		{
+			TimeRange[] targetRanges = new TimeRange[ranges.Length];
+			for (int i = 0; i < ranges.Length; i++)
+			{
+				targetRanges[i].startTime = ranges[i].start;
+				targetRanges[i].duration = ranges[i].duration;
+			}
+			return new TimeRanges(targetRanges);
+		}
+	}
+
+	// IMediaControl
+	public sealed partial class AppleMediaPlayer
+	{
+		public override bool OpenMedia(string path, long offset, string headers, MediaHints mediaHints, int forceFileFormat, bool startWithHighestBitrate)
+		{
+			_mediaHints = mediaHints;
+
+			bool b = false;
+			Match match = RxSupportedSchema.Match(path);
+			if (match.Success)
+			{
+				string schema = match.Value;
+				if (schema == "http://" || schema == "https://")
+				{
+					b = Native.AVPPlayerOpenURL(_player, path, headers);
+				}
+				else if (schema == "file://")
+				{
+					b = Native.AVPPlayerOpenURL(_player, path, null);
+				}
+				else
+				{
+					Debug.LogWarningFormat("[AVProVideo] Unsupported schema '{0}'", schema);
+				}
+			}
+			else if (path.StartsWith("/"))
+			{
+				b = Native.AVPPlayerOpenURL(_player, "file://" + path, null);
+			}
+			else
+			{
+				Debug.LogWarning("[AVProVideo] Path is not a URL nor is it absolute.");
+			}
+
+			if (b)
+			{
+				Update();
+			}
+
+			return b;
+		}
+
+		public override bool OpenMediaFromBuffer(byte[] buffer)
+		{
+			// Unsupported
+			return false;
+		}
+
+		public override bool StartOpenMediaFromBuffer(ulong length)
+		{
+			// Unsupported
+			return false;
+		}
+
+		public override bool AddChunkToMediaBuffer(byte[] chunk, ulong offset, ulong length)
+		{
+			// Unsupported
+			return false;
+		}
+
+		public override bool EndOpenMediaFromBuffer()
+		{
+			// Unsupported
+			return false;
+		}
+
+		public override void CloseMedia()
+		{
+			Native.AVPPlayerClose(_player);
+			Update();
+
+			// Clean up the textures
+			for (int i = 0; i < MaxTexturePlanes; ++i)
+			{
+				if (_texturePlanes[i] != null)
+				{
+					_texturePlanes[i].UpdateExternalTexture(IntPtr.Zero);
+					_texturePlanes[i] = null;
+				}
+			}
+			_playerTexture.frameCount = 0;
+		}
+
+		public override void SetLooping(bool b)
+		{
+			_flags = _flags.SetLooping(b);
+		}
+
+		public override bool IsLooping()
+		{
+			return _flags.IsLooping();
+		}
+
+		public override bool HasMetaData()
+		{
+			return _state.status.HasMetadata();
+		}
+
+		public override bool CanPlay()
+		{
+			return _state.status.IsReadyToPlay();
+		}
+
+		public override bool IsPlaying()
+		{
+			return _state.status.IsPlaying();
+		}
+
+		public override bool IsSeeking()
+		{
+			return _state.status.IsSeeking() || _state.status.HasFinishedSeeking();
+		}
+
+		public override bool IsPaused()
+		{
+			return _state.status.IsPaused();
+		}
+
+		public override bool IsFinished()
+		{
+			return _state.status.IsFinished();
+		}
+
+		public override bool IsBuffering()
+		{
+			return _state.status.IsBuffering();
+		}
+
+		public override void Play()
+		{
+			Native.AVPPlayerSetRate(_player, _rate);
+			Update();
+		}
+
+		public override void Pause()
+		{
+			Native.AVPPlayerSetRate(_player, 0.0f);
+			Update();
+		}
+
+		public override void Stop()
+		{
+			Pause();
+		}
+
+		public override void Rewind()
+		{
+			SeekWithTolerance(0.0, 0.0, 0.0);
+		}
+
+		public override void Seek(double toTime)
+		{
+			SeekWithTolerance(toTime, 0.0, 0.0);
+		}
+
+		public override void SeekFast(double toTime)
+		{
+			SeekWithTolerance(toTime, double.PositiveInfinity, double.PositiveInfinity);
+		}
+
+		public override void SeekWithTolerance(double toTime, double toleranceBefore, double toleranceAfter)
+		{
+			Native.AVPPlayerSeek(_player, toTime, toleranceBefore, toleranceAfter);
+			Update();
+		}
+
+		public override double GetCurrentTime()
+		{
+			return _state.currentTime;
+		}
+
+		public override DateTime GetProgramDateTime()
+		{
+			return Epoch.AddSeconds(_state.currentDate);
+		}
+
+		public override float GetPlaybackRate()
+		{
+			return _rate;
+		}
+
+		public override void SetPlaybackRate(float rate)
+		{
+			if (rate != _rate)
+			{
+				_rate = rate;
+				Native.AVPPlayerSetRate(_player, rate);
+				Update();
+			}
+		}
+
+		public override void MuteAudio(bool mute)
+		{
+			_flags = _flags.SetMuted(mute);
+		}
+
+		public override bool IsMuted()
+		{
+			return _flags.IsMuted();
+		}
+
+		public override void SetVolume(float volume)
+		{
+			if (volume != _volume)
+			{
+				_volume = volume;
+				Native.AVPPlayerSetVolume(_player, volume);
+			}
+		}
+
+		public override void SetBalance(float balance)
+		{
+			// Unsupported
+		}
+
+		public override float GetVolume()
+		{
+			return _volume;
+		}
+
+		public override float GetBalance()
+		{
+			// Unsupported
+			return 0.0f;
+		}
+
+		public override long GetLastExtendedErrorCode()
+		{
+			return 0;
+		}
+
+		public override int GetAudioChannelCount()
+		{
+			int channelCount = -1;
+			if (_state.selectedAudioTrack > -1 && _state.selectedAudioTrack < _audioTrackInfo.Length)
+			{
+				channelCount = (int)_audioTrackInfo[_state.selectedAudioTrack].channelCount;
+				#if !UNITY_EDITOR && UNITY_IOS
+					if (_options.audioMode == MediaPlayer.OptionsApple.AudioMode.Unity)
+					{
+						// iOS audio capture will convert down to two channel stereo
+						channelCount = Math.Min(channelCount, 2);
+					}
+				#endif
+			}
+			return channelCount;
+		}
+
+		public override AudioChannelMaskFlags GetAudioChannelMask()
+		{
+			if (_state.selectedAudioTrack != -1 && _state.selectedAudioTrack < _audioTrackInfo.Length)
+			{
+				return _audioTrackInfo[_state.selectedAudioTrack].channelBitmap;
+			}
+			return AudioChannelMaskFlags.Unspecified;
+		}
+
+		public override void AudioConfigurationChanged(bool deviceChanged)
+		{
+			if (_playerSettings.audioOutputMode == Native.AVPPlayerAudioOutputMode.SystemDirect)
+				return;
+			_playerSettings.sampleRate = AudioSettings.outputSampleRate;
+			int numBuffers;
+			AudioSettings.GetDSPBufferSize(out _playerSettings.bufferLength, out numBuffers);
+			Native.AVPPlayerSetPlayerSettings(_player, _playerSettings);
+		}
+
+		public override int GrabAudio(float[] buffer, int sampleCount, int channelCount)
+		{
+			return Native.AVPPlayerGetAudio(_player, buffer, buffer.Length);
+		}
+
+		public override int GetAudioBufferedSampleCount()
+		{
+			return _state.audioCaptureBufferedSamplesCount;
+		}
+
+		public override void SetAudioHeadRotation(Quaternion q)
+		{
+			// Unsupported
+		}
+
+		public override void ResetAudioHeadRotation()
+		{
+			// Unsupported
+		}
+
+		public override void SetAudioChannelMode(Audio360ChannelMode channelMode)
+		{
+			// Unsupported
+		}
+
+		public override void SetAudioFocusEnabled(bool enabled)
+		{
+			// Unsupported
+		}
+
+		public override void SetAudioFocusProperties(float offFocusLevel, float widthDegrees)
+		{
+			// Unsupported
+		}
+
+		public override void SetAudioFocusRotation(Quaternion q)
+		{
+			// Unsupported
+		}
+
+		public override void ResetAudioFocus()
+		{
+			// Unsupported
+		}
+
+		public override bool WaitForNextFrame(Camera camera, int previousFrameCount)
+		{
+			return false;
+		}
+
+		public override void SetKeyServerAuthToken(string token)
+		{
+			Native.AVPPlayerSetKeyServerAuthToken(_player, token);
+		}
+
+		public override void SetOverrideDecryptionKey(byte[] key)
+		{
+			int length = key != null ? key.Length : 0;
+			Native.AVPPlayerSetDecryptionKey(_player, key, length);
+		}
+
+		public override bool IsExternalPlaybackActive()
+		{
+			return _state.status.IsExternalPlaybackActive();
+		}
+
+		public override void SetAllowsExternalPlayback(bool enable)
+		{
+			_flags.SetAllowExternalPlayback(enable);
+		}
+
+		public override void SetExternalPlaybackVideoGravity(ExternalPlaybackVideoGravity gravity_)
+		{
+			Native.AVPPlayerExternalPlaybackVideoGravity gravity;
+			switch (gravity_)
+			{
+				case ExternalPlaybackVideoGravity.Resize:
+				default:
+					gravity = Native.AVPPlayerExternalPlaybackVideoGravity.Resize;
+					break;
+				case ExternalPlaybackVideoGravity.ResizeAspect:
+					gravity = Native.AVPPlayerExternalPlaybackVideoGravity.ResizeAspect;
+					break;
+				case ExternalPlaybackVideoGravity.ResizeAspectFill:
+					gravity = Native.AVPPlayerExternalPlaybackVideoGravity.ResizeAspectFill;
+					break;
+			}
+			Native.AVPPlayerSetExternalPlaybackVideoGravity(_player, gravity);
+		}
+	}
+
+	// IMediaInfo
+	public sealed partial class AppleMediaPlayer
+	{
+		public override double GetDuration()
+		{
+			return _assetInfo.duration;
+		}
+
+		public override int GetVideoWidth()
+		{
+			int width = 0;
+			if (_state.selectedVideoTrack >= 0)
+			{
+				width = (int)_videoTrackInfo[_state.selectedVideoTrack].dimensions.width;
+			}
+			return width;
+		}
+
+		public override int GetVideoHeight()
+		{
+			int height = 0;
+			if (_state.selectedVideoTrack >= 0)
+			{
+				height = (int)_videoTrackInfo[_state.selectedVideoTrack].dimensions.height;
+			}
+			return height;
+		}
+
+		public override float GetVideoFrameRate()
+		{
+			float framerate = 0.0f;
+			if (_state.selectedVideoTrack >= 0)
+			{
+				framerate = _videoTrackInfo[_state.selectedVideoTrack].frameRate;
+			}
+			return framerate;
+		}
+
+		public override bool HasVideo()
+		{
+			return _state.status.HasVideo();
+		}
+
+		public override bool HasAudio()
+		{
+			return _state.status.HasAudio();
+		}
+
+		public override bool PlayerSupportsLinearColorSpace()
+		{
+			return _playerTexture.flags.IsLinear();
+		}
+
+		public override bool IsPlaybackStalled()
+		{
+			return _state.status.IsStalled();
+		}
+
+		public override float[] GetTextureTransform()
+		{
+			if (_state.selectedVideoTrack >= 0)
+			{
+				Native.AVPAffineTransform transform = _videoTrackInfo[_state.selectedVideoTrack].transform;
+				return new float[] { transform.a, transform.b, transform.c, transform.d, transform.tx, transform.ty };
+			}
+			else
+			{
+				return new float[] { 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f };
+			}
+		}
+
+		public override long GetEstimatedTotalBandwidthUsed()
+		{
+			return 0;
+		}
+
+		public override bool IsExternalPlaybackSupported()
+		{
+			return _assetInfo.flags.IsCompatibleWithAirPlay();
+		}
+	}
+
+	// IMediaProducer
+	public sealed partial class AppleMediaPlayer
+	{
+		public override int GetTextureCount()
+		{
+			return _playerTexture.planeCount;
+		}
+
+		public override Texture GetTexture(int index)
+		{
+			return _texturePlanes[index];
+		}
+
+		public override int GetTextureFrameCount()
+		{
+			return _playerTexture.frameCount;
+		}
+
+		public override bool SupportsTextureFrameCount()
+		{
+			return true;
+		}
+
+		public override long GetTextureTimeStamp()
+		{
+			return _playerTexture.itemTime;
+		}
+
+		public override bool RequiresVerticalFlip()
+		{
+			return _playerTexture.flags.IsFlipped();
+		}
+
+		public override TransparencyMode GetTextureTransparency()
+		{
+			if (_state.selectedVideoTrack >= 0)
+			{
+				Native.AVPPlayerVideoTrackInfo info = _videoTrackInfo[_state.selectedVideoTrack];
+				if ((info.videoTrackFlags & Native.AVPPlayerVideoTrackFlags.HasAlpha) == Native.AVPPlayerVideoTrackFlags.HasAlpha)
+				{
+					return TransparencyMode.Transparent;
+				}
+			}
+			return base.GetTextureTransparency();
+		}
+
+		public override Matrix4x4 GetYpCbCrTransform()
+		{
+			if (_videoTrackInfo.Length > 0 && _state.selectedVideoTrack >= 0)
+				return _videoTrackInfo[_state.selectedVideoTrack].yCbCrTransform;
+			else
+				return Matrix4x4.identity;
+		}
+
+		internal override StereoPacking InternalGetTextureStereoPacking()
+		{
+			if (_state.selectedVideoTrack >= 0)
+			{
+				switch (_videoTrackInfo[_state.selectedVideoTrack].stereoMode)
+				{
+					case Native.AVPPlayerVideoTrackStereoMode.Unknown:
+						return StereoPacking.Unknown;
+					case Native.AVPPlayerVideoTrackStereoMode.Monoscopic:
+						return StereoPacking.None;
+					case Native.AVPPlayerVideoTrackStereoMode.StereoscopicLeftRight:
+						return StereoPacking.LeftRight;
+					case Native.AVPPlayerVideoTrackStereoMode.StereoscopicTopBottom:
+						return StereoPacking.TopBottom;
+					case Native.AVPPlayerVideoTrackStereoMode.StereoscopicRightLeft:
+						return StereoPacking.Unknown;
+					case Native.AVPPlayerVideoTrackStereoMode.StereoscopicCustom:
+						return StereoPacking.CustomUV;
+				}
+			}
+			return StereoPacking.Unknown;
+		}
+	}
+
+	// IDispose
+	public sealed partial class AppleMediaPlayer
+	{
+		public override void Dispose()
+		{
+			Native.AVPPlayerRelease(_player);
+		}
+	}
+
+	// Version
+	public sealed partial class AppleMediaPlayer
+	{
+		public override string GetVersion()
+		{
+			return Native.GetPluginVersion();
+		}
+
+		public override string GetExpectedVersion()
+		{
+			return Helper.ExpectedPluginVersion.Apple;
+		}
+	}
+
+	// Media selection
+	public sealed partial class AppleMediaPlayer
+	{
+		internal override bool InternalIsChangedTracks(TrackType trackType)
+		{
+			return _state.status.HasUpdatedAssetInfo();
+		}
+
+		internal override int InternalGetTrackCount(TrackType trackType)
+		{
+			switch (trackType)
+			{
+				case TrackType.Video:
+					return _videoTrackInfo.Length;
+				case TrackType.Audio:
+					return _audioTrackInfo.Length;
+				case TrackType.Text:
+					return _textTrackInfo.Length;
+				default:
+					return 0;
+			}
+		}
+
+		internal override bool InternalSetActiveTrack(TrackType trackType, int index)
+		{
+			switch (trackType)
+			{
+				case TrackType.Video:
+					return Native.AVPPlayerSetTrack(_player, Native.AVPPlayerTrackType.Video, index);
+
+				case TrackType.Audio:
+					return Native.AVPPlayerSetTrack(_player, Native.AVPPlayerTrackType.Audio, index);
+
+				case TrackType.Text:
+					return Native.AVPPlayerSetTrack(_player, Native.AVPPlayerTrackType.Text, index);
+
+				default:
+					return false;
+			}
+		}
+
+		internal override TrackBase InternalGetTrackInfo(TrackType type, int index, ref bool isActiveTrack)
+		{
+			TrackBase track = null;
+			switch (type)
+			{
+				case TrackType.Video:
+					if (index >= 0 && index < _videoTrackInfo.Length)
+					{
+						Native.AVPPlayerVideoTrackInfo trackInfo = _videoTrackInfo[index];
+						track = new VideoTrack(index, trackInfo.name, trackInfo.language, trackInfo.flags.IsDefault());
+						isActiveTrack = _state.selectedVideoTrack == index;
+					}
+					break;
+
+				case TrackType.Audio:
+					if (index >= 0 && index < _audioTrackInfo.Length)
+					{
+						Native.AVPPlayerAudioTrackInfo trackInfo = _audioTrackInfo[index];
+						track = new AudioTrack(index, trackInfo.name, trackInfo.language, trackInfo.flags.IsDefault());
+						isActiveTrack = _state.selectedAudioTrack == index;
+					}
+					break;
+
+				case TrackType.Text:
+					if (index >= 0 && index < _textTrackInfo.Length)
+					{
+						Native.AVPPlayerTextTrackInfo trackInfo = _textTrackInfo[index];
+						track = new TextTrack(index, trackInfo.name, trackInfo.language, trackInfo.flags.IsDefault());
+						isActiveTrack = _state.selectedTextTrack == index;
+					}
+					break;
+
+				default:
+					break;
+			}
+			return track;
+		}
+
+		internal override bool InternalIsChangedTextCue()
+		{
+			return _state.status.HasUpdatedText();
+		}
+
+		internal override string InternalGetCurrentTextCue()
+		{
+			if (_playerText.buffer != IntPtr.Zero)
+				return Marshal.PtrToStringUni(_playerText.buffer, _playerText.length);
+			else
+				return null;
+		}
+	}
+
+#if !UNITY_EDITOR && UNITY_IOS
+	// Media Caching
+	public sealed partial class AppleMediaPlayer
+	{
+        public override bool IsMediaCachingSupported()
+        {
+            return true;
+        }
+
+		public override void AddMediaToCache(string url, string headers, MediaCachingOptions options)
+		{
+			Native.MediaCachingOptions nativeOptions = new Native.MediaCachingOptions();
+			GCHandle artworkHandle = new GCHandle();
+
+			if (options != null)
+			{
+				nativeOptions.minimumRequiredBitRate = options.minimumRequiredBitRate;
+				nativeOptions.minimumRequiredResolution_width = options.minimumRequiredResolution.x;
+				nativeOptions.minimumRequiredResolution_height = options.minimumRequiredResolution.y;
+				nativeOptions.title = options.title;
+				if (options.artwork != null && options.artwork.Length > 0)
+				{
+					artworkHandle = GCHandle.Alloc(options.artwork, GCHandleType.Pinned);
+					nativeOptions.artwork = artworkHandle.AddrOfPinnedObject();
+					nativeOptions.artworkLength = options.artwork.Length;
+				}
+			}
+
+			Native.AVPPluginCacheMediaForURL(url, headers, nativeOptions);
+
+			if (artworkHandle.IsAllocated)
+			{
+				artworkHandle.Free();
+			}
+		}
+
+		public override void CancelDownloadOfMediaToCache(string url)
+		{
+			Native.AVPPluginCancelDownloadOfMediaForURL(url);
+		}
+
+		public override void RemoveMediaFromCache(string url)
+		{
+			Native.AVPPluginRemoveCachedMediaForURL(url);
+		}
+
+        public override CachedMediaStatus GetCachedMediaStatus(string url, ref float progress)
+        {
+			return (CachedMediaStatus)Native.AVPPluginGetCachedMediaStatusForURL(url, ref progress);
+        }
+	}
+#endif
+}
+
+#endif

+ 12 - 0
Assets/AVProVideo/Runtime/Scripts/Internal/Players/AppleMediaPlayer.cs.meta

@@ -0,0 +1,12 @@
+fileFormatVersion: 2
+guid: 3f68628a1ef6349648e502d1c66b5114
+timeCreated: 1547113004
+licenseType: Free
+MonoImporter:
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 226 - 0
Assets/AVProVideo/Runtime/Scripts/Internal/Players/AppleMediaPlayerExtensions.cs

@@ -0,0 +1,226 @@
+//-----------------------------------------------------------------------------
+// Copyright 2015-2022 RenderHeads Ltd.  All rights reserved.
+//-----------------------------------------------------------------------------
+
+#if UNITY_2017_2_OR_NEWER && (UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || (!UNITY_EDITOR && (UNITY_IOS || UNITY_TVOS)))
+
+using System;
+using System.Runtime.InteropServices;
+using UnityEngine;
+
+namespace RenderHeads.Media.AVProVideo
+{
+	internal static class AppleMediaPlayerExtensions
+	{
+		// AVPPlayerStatus
+
+		internal static bool IsReadyToPlay(this AppleMediaPlayer.Native.AVPPlayerStatus status)
+		{
+			return (status & AppleMediaPlayer.Native.AVPPlayerStatus.ReadyToPlay) == AppleMediaPlayer.Native.AVPPlayerStatus.ReadyToPlay;
+		}
+
+		internal static bool IsPlaying(this AppleMediaPlayer.Native.AVPPlayerStatus status)
+		{
+			return (status & AppleMediaPlayer.Native.AVPPlayerStatus.Playing) == AppleMediaPlayer.Native.AVPPlayerStatus.Playing;
+		}
+
+		internal static bool IsPaused(this AppleMediaPlayer.Native.AVPPlayerStatus status)
+		{
+			return (status & AppleMediaPlayer.Native.AVPPlayerStatus.Paused) == AppleMediaPlayer.Native.AVPPlayerStatus.Paused;
+		}
+
+		internal static bool IsFinished(this AppleMediaPlayer.Native.AVPPlayerStatus status)
+		{
+			return (status & AppleMediaPlayer.Native.AVPPlayerStatus.Finished) == AppleMediaPlayer.Native.AVPPlayerStatus.Finished;
+		}
+
+		internal static bool IsSeeking(this AppleMediaPlayer.Native.AVPPlayerStatus status)
+		{
+			return (status & AppleMediaPlayer.Native.AVPPlayerStatus.Seeking) == AppleMediaPlayer.Native.AVPPlayerStatus.Seeking;
+		}
+
+		internal static bool IsBuffering(this AppleMediaPlayer.Native.AVPPlayerStatus status)
+		{
+			return (status & AppleMediaPlayer.Native.AVPPlayerStatus.Buffering) == AppleMediaPlayer.Native.AVPPlayerStatus.Buffering;
+		}
+
+		internal static bool IsStalled(this AppleMediaPlayer.Native.AVPPlayerStatus status)
+		{
+			return (status & AppleMediaPlayer.Native.AVPPlayerStatus.Stalled) == AppleMediaPlayer.Native.AVPPlayerStatus.Stalled;
+		}
+
+		internal static bool IsExternalPlaybackActive(this AppleMediaPlayer.Native.AVPPlayerStatus status)
+		{
+			return (status & AppleMediaPlayer.Native.AVPPlayerStatus.ExternalPlaybackActive) == AppleMediaPlayer.Native.AVPPlayerStatus.ExternalPlaybackActive;
+		}
+
+		internal static bool IsCached(this AppleMediaPlayer.Native.AVPPlayerStatus status)
+		{
+			return (status & AppleMediaPlayer.Native.AVPPlayerStatus.Cached) == AppleMediaPlayer.Native.AVPPlayerStatus.Cached;
+		}
+
+		internal static bool HasFinishedSeeking(this AppleMediaPlayer.Native.AVPPlayerStatus status)
+		{
+			return (status & AppleMediaPlayer.Native.AVPPlayerStatus.FinishedSeeking) == AppleMediaPlayer.Native.AVPPlayerStatus.FinishedSeeking;
+		}
+
+		internal static bool HasUpdatedAssetInfo(this AppleMediaPlayer.Native.AVPPlayerStatus status)
+		{
+			return (status & AppleMediaPlayer.Native.AVPPlayerStatus.UpdatedAssetInfo) == AppleMediaPlayer.Native.AVPPlayerStatus.UpdatedAssetInfo;
+		}
+
+		internal static bool HasUpdatedTexture(this AppleMediaPlayer.Native.AVPPlayerStatus status)
+		{
+			return (status & AppleMediaPlayer.Native.AVPPlayerStatus.UpdatedTexture) == AppleMediaPlayer.Native.AVPPlayerStatus.UpdatedTexture;
+		}
+
+		internal static bool HasUpdatedBufferedTimeRanges(this AppleMediaPlayer.Native.AVPPlayerStatus status)
+		{
+			return (status & AppleMediaPlayer.Native.AVPPlayerStatus.UpdatedBufferedTimeRanges) == AppleMediaPlayer.Native.AVPPlayerStatus.UpdatedBufferedTimeRanges;
+		}
+
+		internal static bool HasUpdatedSeekableTimeRanges(this AppleMediaPlayer.Native.AVPPlayerStatus status)
+		{
+			return (status & AppleMediaPlayer.Native.AVPPlayerStatus.UpdatedSeekableTimeRanges) == AppleMediaPlayer.Native.AVPPlayerStatus.UpdatedSeekableTimeRanges;
+		}
+
+		internal static bool HasUpdatedText(this AppleMediaPlayer.Native.AVPPlayerStatus status)
+		{
+			return (status & AppleMediaPlayer.Native.AVPPlayerStatus.UpdatedText) == AppleMediaPlayer.Native.AVPPlayerStatus.UpdatedText;
+		}
+
+		internal static bool HasVideo(this AppleMediaPlayer.Native.AVPPlayerStatus status)
+		{
+			return (status & AppleMediaPlayer.Native.AVPPlayerStatus.HasVideo) == AppleMediaPlayer.Native.AVPPlayerStatus.HasVideo;
+		}
+
+		internal static bool HasAudio(this AppleMediaPlayer.Native.AVPPlayerStatus status)
+		{
+			return (status & AppleMediaPlayer.Native.AVPPlayerStatus.HasAudio) == AppleMediaPlayer.Native.AVPPlayerStatus.HasAudio;
+		}
+
+		internal static bool HasText(this AppleMediaPlayer.Native.AVPPlayerStatus status)
+		{
+			return (status & AppleMediaPlayer.Native.AVPPlayerStatus.HasText) == AppleMediaPlayer.Native.AVPPlayerStatus.HasText;
+		}
+
+		internal static bool HasMetadata(this AppleMediaPlayer.Native.AVPPlayerStatus status)
+		{
+			return (status & AppleMediaPlayer.Native.AVPPlayerStatus.HasMetadata) == AppleMediaPlayer.Native.AVPPlayerStatus.HasMetadata;
+		}
+
+		internal static bool HasFailed(this AppleMediaPlayer.Native.AVPPlayerStatus status)
+		{
+			return (status & AppleMediaPlayer.Native.AVPPlayerStatus.Failed) == AppleMediaPlayer.Native.AVPPlayerStatus.Failed;
+		}
+
+		// AVPPlayerFlags
+
+		internal static bool IsLooping(this AppleMediaPlayer.Native.AVPPlayerFlags flags)
+		{
+			return (flags & AppleMediaPlayer.Native.AVPPlayerFlags.Looping) == AppleMediaPlayer.Native.AVPPlayerFlags.Looping;
+		}
+
+		internal static AppleMediaPlayer.Native.AVPPlayerFlags SetLooping(this AppleMediaPlayer.Native.AVPPlayerFlags flags, bool b)
+		{
+			if (flags.IsLooping() ^ b)
+			{
+				flags = (b ? flags | AppleMediaPlayer.Native.AVPPlayerFlags.Looping
+				           : flags & ~AppleMediaPlayer.Native.AVPPlayerFlags.Looping) | AppleMediaPlayer.Native.AVPPlayerFlags.Dirty;
+			}
+			return flags;
+		}
+
+		internal static bool IsMuted(this AppleMediaPlayer.Native.AVPPlayerFlags flags)
+		{
+			return (flags & AppleMediaPlayer.Native.AVPPlayerFlags.Muted) == AppleMediaPlayer.Native.AVPPlayerFlags.Muted;
+		}
+
+		internal static AppleMediaPlayer.Native.AVPPlayerFlags SetMuted(this AppleMediaPlayer.Native.AVPPlayerFlags flags, bool b)
+		{
+			if (flags.IsMuted() ^ b)
+			{
+				flags = (b ? flags | AppleMediaPlayer.Native.AVPPlayerFlags.Muted
+				           : flags & ~AppleMediaPlayer.Native.AVPPlayerFlags.Muted) | AppleMediaPlayer.Native.AVPPlayerFlags.Dirty;
+			}
+			return flags;
+		}
+
+		internal static bool IsExternalPlaybackAllowed(this AppleMediaPlayer.Native.AVPPlayerFlags flags)
+		{
+			return (flags & AppleMediaPlayer.Native.AVPPlayerFlags.AllowExternalPlayback) == AppleMediaPlayer.Native.AVPPlayerFlags.AllowExternalPlayback;
+		}
+
+		internal static AppleMediaPlayer.Native.AVPPlayerFlags SetAllowExternalPlayback(this AppleMediaPlayer.Native.AVPPlayerFlags flags, bool b)
+		{
+			if (flags.IsExternalPlaybackAllowed() ^ b)
+			{
+				flags = (b ? flags |  AppleMediaPlayer.Native.AVPPlayerFlags.AllowExternalPlayback
+				           : flags & ~AppleMediaPlayer.Native.AVPPlayerFlags.AllowExternalPlayback) | AppleMediaPlayer.Native.AVPPlayerFlags.Dirty;
+			}
+			return flags;
+		}
+
+		internal static bool ResumePlayback(this AppleMediaPlayer.Native.AVPPlayerFlags flags)
+		{
+			return (flags & AppleMediaPlayer.Native.AVPPlayerFlags.ResumePlayback) == AppleMediaPlayer.Native.AVPPlayerFlags.ResumePlayback;
+		}
+
+		internal static AppleMediaPlayer.Native.AVPPlayerFlags SetResumePlayback(this AppleMediaPlayer.Native.AVPPlayerFlags flags, bool b)
+		{
+			if (flags.ResumePlayback() ^ b)
+			{
+				flags = (b ? flags | AppleMediaPlayer.Native.AVPPlayerFlags.ResumePlayback
+				           : flags & ~AppleMediaPlayer.Native.AVPPlayerFlags.ResumePlayback) | AppleMediaPlayer.Native.AVPPlayerFlags.Dirty;
+			}
+			return flags;
+		}
+
+		internal static bool IsDirty(this AppleMediaPlayer.Native.AVPPlayerFlags flags)
+		{
+			return (flags & AppleMediaPlayer.Native.AVPPlayerFlags.Dirty) == AppleMediaPlayer.Native.AVPPlayerFlags.Dirty;
+		}
+
+		internal static AppleMediaPlayer.Native.AVPPlayerFlags SetDirty(this AppleMediaPlayer.Native.AVPPlayerFlags flags, bool b)
+		{
+			if (flags.IsDirty() ^ b)
+			{
+				flags = b ? flags | AppleMediaPlayer.Native.AVPPlayerFlags.Dirty : flags & ~AppleMediaPlayer.Native.AVPPlayerFlags.Dirty;
+			}
+			return flags;
+		}
+
+		// MARK: AVPPlayerAssetFlags
+
+		internal static bool IsCompatibleWithAirPlay(this AppleMediaPlayer.Native.AVPPlayerAssetFlags flags)
+		{
+			return (flags & AppleMediaPlayer.Native.AVPPlayerAssetFlags.CompatibleWithAirPlay) == AppleMediaPlayer.Native.AVPPlayerAssetFlags.CompatibleWithAirPlay;
+		}
+
+		// MARK: AVPPlayerTrackFlags
+
+		internal static bool IsDefault(this AppleMediaPlayer.Native.AVPPlayerTrackFlags flags)
+		{
+			return (flags & AppleMediaPlayer.Native.AVPPlayerTrackFlags.Default) == AppleMediaPlayer.Native.AVPPlayerTrackFlags.Default;
+		}
+
+		// AVPPlayerTextureFlags
+
+		internal static bool IsFlipped(this AppleMediaPlayer.Native.AVPPlayerTextureFlags flags)
+		{
+			return (flags & AppleMediaPlayer.Native.AVPPlayerTextureFlags.Flipped) == AppleMediaPlayer.Native.AVPPlayerTextureFlags.Flipped;
+		}
+
+		internal static bool IsLinear(this AppleMediaPlayer.Native.AVPPlayerTextureFlags flags)
+		{
+			return (flags & AppleMediaPlayer.Native.AVPPlayerTextureFlags.Linear) == AppleMediaPlayer.Native.AVPPlayerTextureFlags.Linear;
+		}
+
+		internal static bool IsMipmapped(this AppleMediaPlayer.Native.AVPPlayerTextureFlags flags)
+		{
+			return (flags & AppleMediaPlayer.Native.AVPPlayerTextureFlags.Mipmapped) == AppleMediaPlayer.Native.AVPPlayerTextureFlags.Mipmapped;
+		}
+	}
+}
+
+#endif

+ 11 - 0
Assets/AVProVideo/Runtime/Scripts/Internal/Players/AppleMediaPlayerExtensions.cs.meta

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

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