diff --git a/Explorer/Assets/DCL/Audio/Prefabs/MediaPlayer.prefab b/Explorer/Assets/DCL/Audio/Prefabs/MediaPlayer.prefab index 74cb67694fd..170697d3946 100644 --- a/Explorer/Assets/DCL/Audio/Prefabs/MediaPlayer.prefab +++ b/Explorer/Assets/DCL/Audio/Prefabs/MediaPlayer.prefab @@ -11,7 +11,6 @@ GameObject: - component: {fileID: 4404749396010612502} - component: {fileID: 4239607685379854175} - component: {fileID: 112468050678348472} - - component: {fileID: 3132976908783262753} m_Layer: 0 m_Name: MediaPlayer m_TagString: Untagged @@ -43,190 +42,9 @@ MonoBehaviour: m_GameObject: {fileID: 8514532302456129528} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 638c870cac4da414fba921606d504407, type: 3} + m_Script: {fileID: 11500000, guid: 9f507bde81ed0ebf1a2b3c4d5e6f7081, type: 3} m_Name: m_EditorClassIdentifier: - _mediaSource: 1 - _mediaReference: {fileID: 0} - _mediaPath: - _pathType: 0 - _path: https://radioislanegra.org/radio/8010/basic.aac - _fallbackMediaHints: - transparency: 0 - alphaPacking: 0 - stereoPacking: 0 - _autoOpen: 0 - _autoPlayOnStart: 1 - _loop: 0 - _audioVolume: 0.03 - _audioBalance: 0 - _audioMuted: 0 - _playbackRate: 1 - _useResampler: 0 - _resampleMode: 0 - _resampleBufferSize: 5 - _videoMapping: 0 - _textureFilterMode: 1 - _textureWrapMode: 1 - _textureAnisoLevel: 0 - _sideloadSubtitles: 0 - _subtitlePath: - _pathType: 2 - _path: - _audioHeadTransform: {fileID: 0} - _audioFocusEnabled: 0 - _audioFocusTransform: {fileID: 0} - _audioFocusWidthDegrees: 90 - _audioFocusOffLevelDB: 0 - _httpHeaders: - httpHeaders: [] - _keyAuth: - keyServerToken: - overrideDecryptionKeyBase64: - overrideDecryptionKey: - _events: - m_PersistentCalls: - m_Calls: [] - _eventMask: -1 - _pauseMediaOnAppPause: 1 - _playMediaOnAppUnpause: 1 - _persistent: 0 - _forceFileFormat: 0 - _optionsWindows: - httpHeaders: - httpHeaders: [] - keyAuth: - keyServerToken: - overrideDecryptionKeyBase64: - overrideDecryptionKey: - videoApi: 0 - useHardwareDecoding: 1 - useTextureMips: 0 - use10BitTextures: 0 - hintAlphaChannel: 0 - useLowLatency: 0 - useCustomMovParser: 0 - useHapNotchLC: 0 - useStereoDetection: 1 - useTextTrackSupport: 1 - useFacebookAudio360Support: 1 - useAudioDelay: 0 - bufferedFrameSelection: 0 - pauseOnPrerollComplete: 0 - forceAudioOutputDeviceName: - preferredFilters: [] - audioOutput: 0 - audio360ChannelMode: 0 - startWithHighestBitrate: 0 - useLowLiveLatency: 0 - parallelFrameCount: 3 - prerollFrameCount: 4 - useUnityAudio: 0 - enableAudio360: 0 - _optionsMacOSX: - httpHeaders: - httpHeaders: [] - keyAuth: - keyServerToken: - overrideDecryptionKeyBase64: - overrideDecryptionKey: - textureFormat: 0 - _audioMode: 0 - _flags: 0 - maximumPlaybackRate: 2 - _preferredPeakBitRate: 0 - _preferredPeakBitRateUnits: 1 - _preferredForwardBufferDuration: 0 - _preferredMaximumResolution: 0 - _customPreferredMaximumResolution: {x: 0, y: 0} - _optionsIOS: - httpHeaders: - httpHeaders: [] - keyAuth: - keyServerToken: - overrideDecryptionKeyBase64: - overrideDecryptionKey: - textureFormat: 0 - _audioMode: 0 - _flags: 0 - maximumPlaybackRate: 2 - _preferredPeakBitRate: 0 - _preferredPeakBitRateUnits: 1 - _preferredForwardBufferDuration: 0 - _preferredMaximumResolution: 0 - _customPreferredMaximumResolution: {x: 0, y: 0} - _optionsTVOS: - httpHeaders: - httpHeaders: [] - keyAuth: - keyServerToken: - overrideDecryptionKeyBase64: - overrideDecryptionKey: - textureFormat: 0 - _audioMode: 0 - _flags: 0 - maximumPlaybackRate: 2 - _preferredPeakBitRate: 0 - _preferredPeakBitRateUnits: 1 - _preferredForwardBufferDuration: 0 - _preferredMaximumResolution: 0 - _customPreferredMaximumResolution: {x: 0, y: 0} - _optionsAndroid: - httpHeaders: - httpHeaders: [] - keyAuth: - keyServerToken: - overrideDecryptionKeyBase64: - overrideDecryptionKey: - _preferredMaximumResolution: 0 - _customPreferredMaximumResolution: {x: 0, y: 0} - _preferredPeakBitRate: 0 - _preferredPeakBitRateUnits: 1 - videoApi: 2 - useFastOesPath: 0 - showPosterFrame: 0 - audioOutput: 0 - audio360ChannelMode: 0 - audio360LatencyMS: 0 - preferSoftwareDecoder: 0 - forceRtpTCP: 0 - forceEnableMediaCodecAsyncQueueing: 0 - blitTextureFiltering: 0 - fileOffset: 0 - startWithHighestBitrate: 0 - minBufferMs: 50000 - maxBufferMs: 50000 - bufferForPlaybackMs: 2500 - bufferForPlaybackAfterRebufferMs: 5000 - enableAudio360: 0 - _optionsWindowsUWP: - httpHeaders: - httpHeaders: [] - keyAuth: - keyServerToken: - overrideDecryptionKeyBase64: - overrideDecryptionKey: - useHardwareDecoding: 1 - useTextureMips: 0 - use10BitTextures: 0 - hintOutput10Bit: 0 - useLowLatency: 0 - videoApi: 1 - audioOutput: 0 - audio360ChannelMode: 0 - startWithHighestBitrate: 0 - useLowLiveLatency: 0 - _optionsWebGL: - httpHeaders: - httpHeaders: [] - keyAuth: - keyServerToken: - overrideDecryptionKeyBase64: - overrideDecryptionKey: - externalLibrary: 0 - useTextureMips: 0 - m_VideoPath: - m_VideoLocation: 2 --- !u!82 &112468050678348472 AudioSource: m_ObjectHideFlags: 0 @@ -324,19 +142,3 @@ AudioSource: m_PreInfinity: 2 m_PostInfinity: 2 m_RotationOrder: 4 ---- !u!114 &3132976908783262753 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 8514532302456129528} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 3b05a64a5de3f8546bf586f42e37b979, type: 3} - m_Name: - m_EditorClassIdentifier: - _mediaPlayer: {fileID: 4239607685379854175} - _audioOutputMode: 1 - _channelMask: 65535 - _supportPositionalAudio: 0 diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/ECS.Unity.asmdef b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/ECS.Unity.asmdef index 7b5ed161a30..fae7d7c8ec4 100644 --- a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/ECS.Unity.asmdef +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/ECS.Unity.asmdef @@ -31,7 +31,8 @@ "GUID:40cc3d3e494162646bdb002eed05aeec", "GUID:d28a7e4beeca475418c15757abf1b6f1", "GUID:84651a3751eca9349aac36a66bba901b", - "GUID:9e24947de15b9834991c9d8411ea37cf" + "GUID:9e24947de15b9834991c9d8411ea37cf", + "GUID:4a0b2c8f3c9e5f6a0b1c2d3e4f5a6b7c" ], "includePlatforms": [], "excludePlatforms": [], @@ -42,4 +43,4 @@ "defineConstraints": [], "versionDefines": [], "noEngineReferences": false -} +} \ No newline at end of file diff --git a/Explorer/Assets/DCL/PluginSystem/DCL.Plugins.asmdef b/Explorer/Assets/DCL/PluginSystem/DCL.Plugins.asmdef index 2032d9a6b31..92b6c211b6b 100644 --- a/Explorer/Assets/DCL/PluginSystem/DCL.Plugins.asmdef +++ b/Explorer/Assets/DCL/PluginSystem/DCL.Plugins.asmdef @@ -73,8 +73,9 @@ "GUID:63cde67b4a5d47449392dfc49fc0c3ed", "GUID:267f743eefeddd44982a713909628102", "GUID:0e1edbec885c4350bb2ef81c6a413df5", - "GUID:f254b2009e854709a0ef0022a7ca697c", - "GUID:3c1a9be47d0f42e2a5b8c6d4e9f01a27" + "GUID:3c1a9be47d0f42e2a5b8c6d4e9f01a27", + "GUID:4a0b2c8f3c9e5f6a0b1c2d3e4f5a6b7c", + "GUID:f254b2009e854709a0ef0022a7ca697c" ], "includePlatforms": [], "excludePlatforms": [], @@ -90,12 +91,7 @@ "name": "com.dcl.gpui-assets", "expression": "", "define": "GPUI_PRO_PRESENT" - }, - { - "name": "com.renderheads.avpro.video-ultra", - "expression": "", - "define": "AV_PRO_PRESENT" } ], "noEngineReferences": false -} \ No newline at end of file +} diff --git a/Explorer/Assets/DCL/SDKComponents/MediaStream/MediaPlayerCustomPool.cs b/Explorer/Assets/DCL/SDKComponents/MediaStream/MediaPlayerCustomPool.cs index 6e6e8dd1d7c..8f04e5d74b4 100644 --- a/Explorer/Assets/DCL/SDKComponents/MediaStream/MediaPlayerCustomPool.cs +++ b/Explorer/Assets/DCL/SDKComponents/MediaStream/MediaPlayerCustomPool.cs @@ -51,7 +51,6 @@ public MediaPlayer GetOrCreateReusableMediaPlayer(string url) #endif mediaPlayer.PlatformOptionsWindows.startWithHighestBitrate = true; mediaPlayer.PlatformOptionsWindows.useLowLiveLatency = false; - mediaPlayer.AutoOpen = false; mediaPlayer.enabled = true; return mediaPlayer; diff --git a/Explorer/Assets/DCL/SDKComponents/MediaStream/MultiMediaPlayer.cs b/Explorer/Assets/DCL/SDKComponents/MediaStream/MultiMediaPlayer.cs index e996825a9cd..ff773723e20 100644 --- a/Explorer/Assets/DCL/SDKComponents/MediaStream/MultiMediaPlayer.cs +++ b/Explorer/Assets/DCL/SDKComponents/MediaStream/MultiMediaPlayer.cs @@ -29,9 +29,6 @@ public AvProPlayer(MediaPlayer avProMediaPlayer, MediaPlayerCustomPool mediaPlay { this.AvProMediaPlayer = avProMediaPlayer; this.MediaPlayerCustomPool = mediaPlayerCustomPool; - - if (AvProMediaPlayer.TryGetComponent(out AudioSource audioSource)) - AvProMediaPlayer.SetAudioSource(audioSource); } } diff --git a/Explorer/Assets/DCL/SDKComponents/MediaStream/Systems/MediaPlayerPluginWrapper.cs b/Explorer/Assets/DCL/SDKComponents/MediaStream/Systems/MediaPlayerPluginWrapper.cs index c14343258bf..ba43f156e34 100644 --- a/Explorer/Assets/DCL/SDKComponents/MediaStream/Systems/MediaPlayerPluginWrapper.cs +++ b/Explorer/Assets/DCL/SDKComponents/MediaStream/Systems/MediaPlayerPluginWrapper.cs @@ -1,6 +1,7 @@ using Arch.Core; using Arch.SystemGroups; using DCL.CharacterCamera; +using DCL.Diagnostics; using DCL.FeatureFlags; using DCL.Multiplayer.Connections.RoomHubs; using DCL.Optimization.PerformanceBudgeting; @@ -42,7 +43,7 @@ public MediaPlayerPluginWrapper( this.flipMaterial = flipMaterial; this.placeholderSource = placeholderSource; -#if AV_PRO_PRESENT && !UNITY_EDITOR_LINUX && !UNITY_STANDALONE_LINUX +#if !UNITY_EDITOR_LINUX && !UNITY_STANDALONE_LINUX this.frameTimeBudget = frameTimeBudget; #endif } @@ -50,7 +51,9 @@ public MediaPlayerPluginWrapper( public void InjectToWorld(ref ArchSystemsWorldBuilder builder, in ECSWorldInstanceSharedDependencies sceneDeps, IRoomHub roomHub, List finalizeWorldSystems, List sceneIsCurrentListeners) { -#if AV_PRO_PRESENT && !UNITY_EDITOR_LINUX && !UNITY_STANDALONE_LINUX +#if !UNITY_EDITOR_LINUX && !UNITY_STANDALONE_LINUX + ReportHub.LogProductionInfo("[MediaPlayerPluginWrapper] Inject"); + MediaFactory mediaFactory = this.mediaFactory.CreateForScene(builder.World, sceneDeps, roomHub, placeholderSource); CreateMediaPlayerSystem.InjectToWorld(ref builder, sceneDeps.SceneStateProvider, mediaFactory); @@ -62,6 +65,8 @@ public void InjectToWorld(ref ArchSystemsWorldBuilder builder, in ECSWorl VideoEventsSystem.InjectToWorld(ref builder, sceneDeps.EcsToCRDTWriter, sceneDeps.SceneStateProvider, frameTimeBudget); finalizeWorldSystems.Add(CleanUpMediaPlayerSystem.InjectToWorld(ref builder)); +#else + ReportHub.LogProductionInfo("[MediaPlayerPluginWrapper] Ignore Inject"); #endif } diff --git a/Explorer/Assets/DCL/Tests/Editor/DCL.EditMode.Tests.asmdef b/Explorer/Assets/DCL/Tests/Editor/DCL.EditMode.Tests.asmdef index 9b998b9b1c7..f65265bf5b7 100644 --- a/Explorer/Assets/DCL/Tests/Editor/DCL.EditMode.Tests.asmdef +++ b/Explorer/Assets/DCL/Tests/Editor/DCL.EditMode.Tests.asmdef @@ -58,10 +58,10 @@ "GUID:27619889b8ba8c24980f49ee34dbb44a", "GUID:0acc523941302664db1f4e527237feb3", "GUID:4129704b5a1a13841ba16f230bf24a57", - "GUID:3ca842002c51d0a43b73e45298809a13", "GUID:0df5180c0c3a0594fbfa11f83736de9f", "GUID:28964ef7dc9441b6b8671b61a8106690", "GUID:267f743eefeddd44982a713909628102", + "GUID:4a0b2c8f3c9e5f6a0b1c2d3e4f5a6b7c", "GUID:f254b2009e854709a0ef0022a7ca697c" ], "includePlatforms": [ @@ -92,4 +92,4 @@ ], "versionDefines": [], "noEngineReferences": false -} +} \ No newline at end of file diff --git a/Explorer/Assets/Plugins/UUAV.meta b/Explorer/Assets/Plugins/UUAV.meta new file mode 100644 index 00000000000..25a15fc3208 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6b05bb2789c26ac4897c1cf13007848d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages.meta b/Explorer/Assets/Plugins/UUAV/Packages.meta new file mode 100644 index 00000000000..b5d68de7a2c --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0f2609b9cdb7f3a4db478c2732079251 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV.meta new file mode 100644 index 00000000000..deeacaa0fac --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 387654979bb264dbf92195445d7523b2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat.meta new file mode 100644 index 00000000000..d47dabd6334 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3f9a1c7e2b8d4e5f9a0b1c2d3e4f5a6b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/Enums.cs b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/Enums.cs new file mode 100644 index 00000000000..dd3519d7095 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/Enums.cs @@ -0,0 +1,45 @@ +// Effectively NO-OPs, exist only for the compatibility +namespace RenderHeads.Media.AVProVideo +{ + // Mirrors AVPro's MediaPathType. The consumer only ever passes + // AbsolutePathOrURL; the remaining members exist for signature parity. + public enum MediaPathType + { + AbsolutePathOrURL, + RelativeToProjectFolder, + RelativeToStreamingAssetsFolder, + RelativeToDataFolder, + RelativeToPersistentDataFolder, + } + + // Mirrors AVPro's ErrorCode. UUAV surfaces errors only as a state, so the + // backend collapses every failure onto LoadFailed; the consumer just checks + // for != None. + public enum ErrorCode + { + None = 0, + LoadFailed = 100, + DecodeFailed = 200, + } + + // Windows platform-option enums. Inert under UUAV (audio always routes + // through Unity, video always uses the D3D11 path) and present only so the + // consumer's platform-options assignments compile. + public static class Windows + { + public enum VideoApi + { + MediaFoundation, + DirectShow, + WinRT, + } + + public enum AudioOutput + { + System, + Unity, + FacebookAudio360, + None, + } + } +} diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/Enums.cs.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/Enums.cs.meta new file mode 100644 index 00000000000..b9bcf71c948 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/Enums.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6c2d4eab5eba7b8c0d1e2f3a4b5c6d7e diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/Interfaces.cs b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/Interfaces.cs new file mode 100644 index 00000000000..d8d11b17820 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/Interfaces.cs @@ -0,0 +1,60 @@ +using UnityEngine; + +namespace RenderHeads.Media.AVProVideo +{ + // The subset of AVPro's IMediaControl the consumer polls. Members AVPro + // exposes but the consumer never calls (SeekFast, volume/mute, audio-360, + // external playback, ...) are intentionally omitted. + public interface IMediaControl + { + void Play(); + void Pause(); + void Stop(); + void Seek(double time); + + bool IsPlaying(); + bool IsPaused(); + bool IsSeeking(); + bool IsBuffering(); + bool IsFinished(); + + bool IsLooping(); + void SetLooping(bool looping); + + double GetCurrentTime(); + + float GetPlaybackRate(); + void SetPlaybackRate(float rate); + + ErrorCode GetLastError(); + + TimeRanges GetBufferedTimes(); + } + + // The consumer reads only the duration off IMediaInfo (it takes width/height + // from the produced texture instead). + public interface IMediaInfo + { + double GetDuration(); + } + + // The consumer uses only GetTexture + RequiresVerticalFlip. + public interface ITextureProducer + { + Texture? GetTexture(int index = 0); + bool RequiresVerticalFlip(); + } + + // AVPro's TimeRanges exposes an indexer and time bounds; the consumer only + // reads Count, using "buffered range exists" as its readiness gate before + // applying playback properties. + public sealed class TimeRanges + { + public int Count { get; } + + public TimeRanges(int count) + { + Count = count; + } + } +} diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/Interfaces.cs.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/Interfaces.cs.meta new file mode 100644 index 00000000000..9dcec123fcb --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/Interfaces.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7d3e5fbc6fcb8c9d0e1f2a3b4c5d6e7f diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/MediaPlayer.cs b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/MediaPlayer.cs new file mode 100644 index 00000000000..b20d1c7c07f --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/MediaPlayer.cs @@ -0,0 +1,101 @@ +using UnityEngine; +using UUAV; + +namespace RenderHeads.Media.AVProVideo +{ + // AVPro-compatible facade over UUAVPlayer. The consumer is poll-based + // (no event subscriptions), see the compat plan for the full mapping. + [RequireComponent(typeof(AudioSource))] + public sealed class MediaPlayer : MonoBehaviour + { + // Base for the per-platform option objects; holds the shared AudioMode + // enum the macOS options reference. All values are inert under UUAV. + public class PlatformOptions + { + public enum AudioMode + { + SystemDirect, + Unity, + FacebookAudio360, + None, + } + } + + public sealed class OptionsWindows : PlatformOptions + { + public Windows.AudioOutput _audioMode = Windows.AudioOutput.Unity; + public Windows.VideoApi videoApi = Windows.VideoApi.MediaFoundation; + public bool startWithHighestBitrate; + public bool useLowLiveLatency; + } + + public sealed class OptionsApple : PlatformOptions + { + public AudioMode audioMode = AudioMode.Unity; + } + + private UUAVPlayer uuavPlayer = null!; + private UUAVBackend backend = null!; + + public OptionsWindows PlatformOptionsWindows { get; } = new OptionsWindows(); + public OptionsApple PlatformOptions_macOS { get; } = new OptionsApple(); + + public IMediaControl Control => backend; + public IMediaInfo Info => backend; + public ITextureProducer TextureProducer => backend; + public MediaPlayerEvent Events { get; } = new MediaPlayerEvent(); + + public AudioSource AudioSource => uuavPlayer.AudioSource; + + public bool MediaOpened => + uuavPlayer is not null && uuavPlayer.State is not (UUAVState.Closed or UUAVState.Error or UUAVState.Unknown); + + public float AudioVolume + { + get => AudioSource ? AudioSource.volume : 0f; + set + { + if (AudioSource) + { + AudioSource.volume = value; + } + } + } + + private void Awake() + { + uuavPlayer = gameObject.AddComponent(); + backend = new UUAVBackend(uuavPlayer); + } + + private void Update() + { + backend.Tick(); + } + + public bool OpenMedia(MediaPathType pathType, string path, bool autoPlay) + { + // Only AbsolutePathOrURL is used; UUAV takes the URL/path verbatim. + uuavPlayer.OpenMedia(path); + + if (autoPlay) + { + uuavPlayer.Play(); + } + + return true; + } + + // AVPro's Stop resets to the start; the consumer only calls it during + // teardown, so a pause is sufficient. + public void Stop() + { + uuavPlayer.Pause(); + } + + public void CloseMedia() + { + uuavPlayer.CloseMedia(); + } + } +} diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/MediaPlayer.cs.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/MediaPlayer.cs.meta new file mode 100644 index 00000000000..47bf586c717 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/MediaPlayer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9f507bde81ed0ebf1a2b3c4d5e6f7081 diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/MediaPlayerEvent.cs b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/MediaPlayerEvent.cs new file mode 100644 index 00000000000..e7a1b13dbc8 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/MediaPlayerEvent.cs @@ -0,0 +1,12 @@ +namespace RenderHeads.Media.AVProVideo +{ + // The consumer never subscribes to events (it polls Control instead). It + // only touches these two methods, and only for defensive cleanup, so they + // are no-ops. + public sealed class MediaPlayerEvent + { + public bool HasListeners() => false; + + public void RemoveAllListeners() { } + } +} diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/MediaPlayerEvent.cs.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/MediaPlayerEvent.cs.meta new file mode 100644 index 00000000000..9855e9d09a0 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/MediaPlayerEvent.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8e4f6acd70dc9dae0f1a2b3c4d5e6f70 diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/README.md b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/README.md new file mode 100644 index 00000000000..165434fd170 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/README.md @@ -0,0 +1,4 @@ +# Compatability Note + +The compatability layer doesn't provide full coverage of AVPRO features and should be extended as it goes. +Feel free to contribute your adapter code diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/README.md.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/README.md.meta new file mode 100644 index 00000000000..c0f4510eee3 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 367d0253380b0eb47a3fdcd56d0f9b4d +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/UUAV.AVProCompat.asmdef b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/UUAV.AVProCompat.asmdef new file mode 100644 index 00000000000..7c35d84a3be --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/UUAV.AVProCompat.asmdef @@ -0,0 +1,16 @@ +{ + "name": "UUAV.AVProCompat", + "rootNamespace": "RenderHeads.Media.AVProVideo", + "references": [ + "GUID:d9709898aa920584a8d1613950e6a7f5" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/UUAV.AVProCompat.asmdef.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/UUAV.AVProCompat.asmdef.meta new file mode 100644 index 00000000000..dd2a37f6c06 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/UUAV.AVProCompat.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4a0b2c8f3c9e5f6a0b1c2d3e4f5a6b7c +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/UUAVBackend.cs b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/UUAVBackend.cs new file mode 100644 index 00000000000..fe4ed195e96 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/UUAVBackend.cs @@ -0,0 +1,102 @@ +using UnityEngine; +using UUAV; + +namespace RenderHeads.Media.AVProVideo +{ + internal sealed class UUAVBackend : IMediaControl, IMediaInfo, ITextureProducer + { + private static readonly TimeRanges NotBuffered = new TimeRanges(0); + private static readonly TimeRanges Buffered = new TimeRanges(1); + + private readonly UUAVPlayer player; + + private bool seekRequested; + + public UUAVBackend(UUAVPlayer player) + { + this.player = player; + } + + public void Tick() + { + UUAVState state = player.State; + + if (seekRequested + && (state == UUAVState.Playing + || state == UUAVState.Paused + || state == UUAVState.Ready)) + { + seekRequested = false; + } + } + + public void Play() => player.Play(); + + public void Pause() => player.Pause(); + + public void Stop() + { + player.Pause(); + player.Seek(0); // To match AVPro semantics + } + + public void Seek(double time) + { + seekRequested = true; + player.Seek(time); + } + + public bool IsPlaying() => player.State == UUAVState.Playing; + + public bool IsPaused() => player.State == UUAVState.Paused; + + // Best-effort: UUAV has no distinct seeking state, so this reflects a + // pending Seek until the player settles (see Tick). + public bool IsSeeking() => seekRequested; + + public bool IsBuffering() => player.State == UUAVState.Opening; + + public bool IsFinished() => player.State == UUAVState.Ended; + + public bool IsLooping() => player.Looping; + + public void SetLooping(bool value) => player.Looping = value; + + public double GetCurrentTime() => player.CurrentTime; + + public float GetPlaybackRate() => (float)player.PlaybackRate; + + public void SetPlaybackRate(float rate) => player.PlaybackRate = rate; + + public ErrorCode GetLastError() => + player.State == UUAVState.Error ? ErrorCode.LoadFailed : ErrorCode.None; + + // The consumer reads only Count, waiting for a buffered range to exist + // before applying loop/rate/volume. Report one range once media is ready. + public TimeRanges GetBufferedTimes() + { + switch (player.State) + { + case UUAVState.Ready: + case UUAVState.Playing: + case UUAVState.Paused: + case UUAVState.Ended: + return Buffered; + default: + return NotBuffered; + } + } + + public double GetDuration() => player.Duration; + + public Texture? GetTexture(int index = 0) + { + return player.CurrentTexture; + } + + // UUAV's NV12ToRGB shader already flips vertically, so the output + // RenderTexture is upright and no consumer-side flip is needed. + // Comment is valid as long as behaviour of the UUAVPlayer and shader remain unchanged + public bool RequiresVerticalFlip() => false; + } +} diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/UUAVBackend.cs.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/UUAVBackend.cs.meta new file mode 100644 index 00000000000..de1343ea6be --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/UUAVBackend.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a0618cef92fe1fca2b3c4d5e6f708192 diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/csc.rsp b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/csc.rsp new file mode 100644 index 00000000000..2d66b0292b8 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/csc.rsp @@ -0,0 +1 @@ +-nullable:enable diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/csc.rsp.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/csc.rsp.meta new file mode 100644 index 00000000000..9828c227403 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/AVProCompat/csc.rsp.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 5b1c3d9a4daf6a7b0c1d2e3f4a5b6c7d +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example.meta new file mode 100644 index 00000000000..add3b7681e0 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f01d9f220edfa0943a0ad9670e48142f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example/Button.prefab b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example/Button.prefab new file mode 100644 index 00000000000..6efd04bbae4 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example/Button.prefab @@ -0,0 +1,260 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &6434394354246174534 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2195277981447782603} + - component: {fileID: 7952421406003065560} + - component: {fileID: 3952792614493521208} + - component: {fileID: 2455051764364230330} + m_Layer: 5 + m_Name: Button + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2195277981447782603 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6434394354246174534} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1200154475737900899} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &7952421406003065560 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6434394354246174534} + m_CullTransparentMesh: 1 +--- !u!114 &3952792614493521208 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6434394354246174534} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!114 &2455051764364230330 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6434394354246174534} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Button + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 3952792614493521208} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!1 &8037599242812101729 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1200154475737900899} + - component: {fileID: 2630665757706217724} + - component: {fileID: 3793433118894115418} + m_Layer: 5 + m_Name: Text (TMP) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1200154475737900899 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8037599242812101729} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 2195277981447782603} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &2630665757706217724 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8037599242812101729} + m_CullTransparentMesh: 1 +--- !u!114 &3793433118894115418 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8037599242812101729} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: Button + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4281479730 + m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 48 + m_fontSizeBase: 48 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_characterHorizontalScale: 1 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 6e72656b + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example/Button.prefab.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example/Button.prefab.meta new file mode 100644 index 00000000000..c14c269b2cb --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example/Button.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 0ea93280ccf09b24bb29d037adb0d0ab +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example/UIExamplePlayer.cs b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example/UIExamplePlayer.cs new file mode 100644 index 00000000000..aa4d3798052 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example/UIExamplePlayer.cs @@ -0,0 +1,55 @@ +using UUAV; + +using System; +using TMPro; +using UnityEngine; +using UnityEngine.UI; +using UnityEngine.Assertions; + +namespace UUAV.Example +{ + public class UIExamplePlayer : MonoBehaviour + { + [SerializeField] private RawImage surface; + [SerializeField] private Button openButton; + [SerializeField] private Button playButton; + [SerializeField] private Button pauseButton; + [SerializeField] private Button closeButton; + [SerializeField] private Toggle loopingToggle; + [SerializeField] private TMP_InputField urlInput; + [SerializeField] private TMP_Text statusText; + + + [Header("Debug")] + [SerializeField] private UUAVPlayer player; + + private void Awake() + { + Assert.IsNotNull(surface); + Assert.IsNotNull(openButton); + Assert.IsNotNull(playButton); + Assert.IsNotNull(pauseButton); + Assert.IsNotNull(closeButton); + Assert.IsNotNull(urlInput); + Assert.IsNotNull(statusText); + Assert.IsNotNull(loopingToggle); + + player = UUAVPlayer.New(); + + openButton.onClick.AddListener(() => player.OpenMedia(urlInput.text)); + playButton.onClick.AddListener(() => player.Play()); + pauseButton.onClick.AddListener(() => player.Pause()); + closeButton.onClick.AddListener(() => player.CloseMedia()); + + loopingToggle.onValueChanged.AddListener(v => player.Looping = v); + player.Looping = loopingToggle.isOn; + } + + private void Update() + { + surface.texture = player.CurrentTexture; + statusText.text = player.State.ToStringNoAlloc(); + } + } +} + diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example/UIExamplePlayer.cs.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example/UIExamplePlayer.cs.meta new file mode 100644 index 00000000000..19ba1046722 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example/UIExamplePlayer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d4cba627e163aad40b85895022059c94 \ No newline at end of file diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example/UUAVExample.asmdef b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example/UUAVExample.asmdef new file mode 100644 index 00000000000..bc927666e7e --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example/UUAVExample.asmdef @@ -0,0 +1,17 @@ +{ + "name": "UUAVExample", + "rootNamespace": "UUAVExample", + "references": [ + "GUID:d9709898aa920584a8d1613950e6a7f5", + "GUID:6055be8ebefd69e48b49212b09b47b2f" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example/UUAVExample.asmdef.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example/UUAVExample.asmdef.meta new file mode 100644 index 00000000000..524e94ad186 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example/UUAVExample.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ba32493aba3c69a49b93662e0687ef66 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example/UUAVExample.unity b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example/UUAVExample.unity new file mode 100644 index 00000000000..388bbfb9a9d --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example/UUAVExample.unity @@ -0,0 +1,2541 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 10 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_BakeOnSceneLoad: 0 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &85335130 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 85335131} + - component: {fileID: 85335133} + - component: {fileID: 85335132} + m_Layer: 5 + m_Name: RawImage + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &85335131 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 85335130} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 771461370} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 720} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &85335132 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 85335130} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1344c3c82d62a2a41a3576d8abb8e3ea, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.RawImage + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Texture: {fileID: 0} + m_UVRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 +--- !u!222 &85335133 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 85335130} + m_CullTransparentMesh: 1 +--- !u!1 &327973004 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 327973005} + - component: {fileID: 327973008} + - component: {fileID: 327973007} + - component: {fileID: 327973006} + m_Layer: 5 + m_Name: Placeholder + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &327973005 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 327973004} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1190593601} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &327973006 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 327973004} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement + m_IgnoreLayout: 1 + m_MinWidth: -1 + m_MinHeight: -1 + m_PreferredWidth: -1 + m_PreferredHeight: -1 + m_FlexibleWidth: -1 + m_FlexibleHeight: -1 + m_LayoutPriority: 1 +--- !u!114 &327973007 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 327973004} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: Enter url... + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 2150773298 + m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 0.5} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 46 + m_fontSizeBase: 46 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 2 + m_HorizontalAlignment: 1 + m_VerticalAlignment: 256 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_characterHorizontalScale: 1 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 0 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 6e72656b + m_enableExtraPadding: 1 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!222 &327973008 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 327973004} + m_CullTransparentMesh: 1 +--- !u!1 &330585543 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 330585546} + - component: {fileID: 330585545} + - component: {fileID: 330585544} + - component: {fileID: 330585547} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &330585544 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 330585543} + m_Enabled: 1 +--- !u!20 &330585545 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 330585543} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 2 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 0 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &330585546 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 330585543} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &330585547 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 330585543} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_RenderShadows: 1 + m_RequiresDepthTextureOption: 2 + m_RequiresOpaqueTextureOption: 2 + m_CameraType: 0 + m_Cameras: [] + m_RendererIndex: -1 + m_VolumeLayerMask: + serializedVersion: 2 + m_Bits: 1 + m_VolumeTrigger: {fileID: 0} + m_VolumeFrameworkUpdateModeOption: 2 + m_RenderPostProcessing: 1 + m_Antialiasing: 0 + m_AntialiasingQuality: 2 + m_StopNaN: 0 + m_Dithering: 0 + m_ClearDepth: 1 + m_AllowXRRendering: 1 + m_AllowHDROutput: 1 + m_UseScreenCoordOverride: 0 + m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0} + m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0} + m_RequiresDepthTexture: 0 + m_RequiresColorTexture: 0 + m_TaaSettings: + m_Quality: 3 + m_FrameInfluence: 0.1 + m_JitterScale: 1 + m_MipBias: 0 + m_VarianceClampScale: 0.9 + m_ContrastAdaptiveSharpening: 0 + m_Version: 2 +--- !u!1 &410087039 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 410087041} + - component: {fileID: 410087040} + - component: {fileID: 410087042} + m_Layer: 0 + m_Name: Directional Light + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!108 &410087040 +Light: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 410087039} + m_Enabled: 1 + serializedVersion: 13 + m_Type: 1 + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_Intensity: 2 + m_Range: 10 + m_SpotAngle: 30 + m_InnerSpotAngle: 21.80208 + m_CookieSize2D: {x: 10, y: 10} + m_Shadows: + m_Type: 2 + m_Resolution: -1 + m_CustomResolution: -1 + m_Strength: 1 + m_Bias: 0.05 + m_NormalBias: 0.4 + m_NearPlane: 0.2 + m_CullingMatrixOverride: + e00: 1 + e01: 0 + e02: 0 + e03: 0 + e10: 0 + e11: 1 + e12: 0 + e13: 0 + e20: 0 + e21: 0 + e22: 1 + e23: 0 + e30: 0 + e31: 0 + e32: 0 + e33: 1 + m_UseCullingMatrixOverride: 0 + m_Cookie: {fileID: 0} + m_DrawHalo: 0 + m_Flare: {fileID: 0} + m_RenderMode: 0 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingLayerMask: 1 + m_Lightmapping: 4 + m_LightShadowCasterMode: 0 + m_AreaSize: {x: 1, y: 1} + m_BounceIntensity: 1 + m_ColorTemperature: 5000 + m_UseColorTemperature: 1 + m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} + m_UseBoundingSphereOverride: 0 + m_UseViewFrustumForShadowCasterCull: 1 + m_ForceVisible: 0 + m_ShapeRadius: 0 + m_ShadowAngle: 0 + m_LightUnit: 1 + m_LuxAtDistance: 1 + m_EnableSpotReflector: 1 +--- !u!4 &410087041 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 410087039} + serializedVersion: 2 + m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} + m_LocalPosition: {x: 0, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0} +--- !u!114 &410087042 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 410087039} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 474bcb49853aa07438625e644c072ee6, type: 3} + m_Name: + m_EditorClassIdentifier: + m_UsePipelineSettings: 1 + m_AdditionalLightsShadowResolutionTier: 2 + m_CustomShadowLayers: 0 + m_LightCookieSize: {x: 1, y: 1} + m_LightCookieOffset: {x: 0, y: 0} + m_SoftShadowQuality: 1 + m_RenderingLayersMask: + serializedVersion: 0 + m_Bits: 1 + m_ShadowRenderingLayersMask: + serializedVersion: 0 + m_Bits: 1 + m_Version: 4 + m_LightLayerMask: 1 + m_ShadowLayerMask: 1 + m_RenderingLayers: 1 + m_ShadowRenderingLayers: 1 +--- !u!1001 &487448423 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 868217820} + m_Modifications: + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_Pivot.x + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_Pivot.y + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_SizeDelta.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalPosition.z + value: -0.549465 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3793433118894115418, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_text + value: 'Close Media + +' + objectReference: {fileID: 0} + - target: {fileID: 6434394354246174534, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_Name + value: Button Close Media + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} +--- !u!224 &487448424 stripped +RectTransform: + m_CorrespondingSourceObject: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + m_PrefabInstance: {fileID: 487448423} + m_PrefabAsset: {fileID: 0} +--- !u!114 &487448425 stripped +MonoBehaviour: + m_CorrespondingSourceObject: {fileID: 2455051764364230330, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + m_PrefabInstance: {fileID: 487448423} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Button +--- !u!1 &602982404 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 602982405} + m_Layer: 5 + m_Name: TextGroup + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &602982405 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 602982404} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1525984651} + - {fileID: 1004410499} + m_Father: {fileID: 771461370} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 100} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &771461366 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 771461370} + - component: {fileID: 771461369} + - component: {fileID: 771461368} + - component: {fileID: 771461367} + - component: {fileID: 771461371} + - component: {fileID: 771461372} + m_Layer: 5 + m_Name: Canvas_UI_Player + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &771461367 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 771461366} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.GraphicRaycaster + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!114 &771461368 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 771461366} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.CanvasScaler + m_UiScaleMode: 1 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 1920, y: 1080} + m_ScreenMatchMode: 0 + m_MatchWidthOrHeight: 0 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 + m_PresetInfoIsWorld: 0 +--- !u!223 &771461369 +Canvas: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 771461366} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 0 + m_Camera: {fileID: 0} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_VertexColorAlwaysGammaSpace: 0 + m_UseReflectionProbes: 0 + m_AdditionalShaderChannelsFlag: 25 + m_UpdateRectTransformForStandalone: 0 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 +--- !u!224 &771461370 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 771461366} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0, y: 0, z: 0} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 85335131} + - {fileID: 868217820} + - {fileID: 602982405} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 0} +--- !u!114 &771461371 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 771461366} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d4cba627e163aad40b85895022059c94, type: 3} + m_Name: + m_EditorClassIdentifier: UUAVExample::UUAV.Example.UIExamplePlayer + surface: {fileID: 85335132} + openButton: {fileID: 786630686} + playButton: {fileID: 1042744300} + pauseButton: {fileID: 1052056180} + closeButton: {fileID: 487448425} + loopingToggle: {fileID: 1420431042} + urlInput: {fileID: 1004410500} + statusText: {fileID: 1525984652} + player: {fileID: 0} +--- !u!114 &771461372 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 771461366} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.VerticalLayoutGroup + m_Padding: + m_Left: 0 + m_Right: 0 + m_Top: 0 + m_Bottom: 0 + m_ChildAlignment: 0 + m_Spacing: 50 + m_ChildForceExpandWidth: 1 + m_ChildForceExpandHeight: 1 + m_ChildControlWidth: 1 + m_ChildControlHeight: 0 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 + m_ReverseArrangement: 0 +--- !u!1001 &786630684 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 868217820} + m_Modifications: + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_Pivot.x + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_Pivot.y + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_SizeDelta.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalPosition.z + value: -0.549465 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3793433118894115418, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_text + value: 'Open Media + +' + objectReference: {fileID: 0} + - target: {fileID: 6434394354246174534, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_Name + value: Button Open Media + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} +--- !u!224 &786630685 stripped +RectTransform: + m_CorrespondingSourceObject: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + m_PrefabInstance: {fileID: 786630684} + m_PrefabAsset: {fileID: 0} +--- !u!114 &786630686 stripped +MonoBehaviour: + m_CorrespondingSourceObject: {fileID: 2455051764364230330, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + m_PrefabInstance: {fileID: 786630684} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Button +--- !u!1 &832575517 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 832575519} + - component: {fileID: 832575518} + m_Layer: 0 + m_Name: Global Volume + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &832575518 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 832575517} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 172515602e62fb746b5d573b38a5fe58, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IsGlobal: 1 + priority: 0 + blendDistance: 0 + weight: 1 + sharedProfile: {fileID: 11400000, guid: 10fc4df2da32a41aaa32d77bc913491c, type: 2} +--- !u!4 &832575519 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 832575517} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &868217819 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 868217820} + - component: {fileID: 868217821} + m_Layer: 5 + m_Name: button_group + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &868217820 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 868217819} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1042744299} + - {fileID: 1052056179} + - {fileID: 786630685} + - {fileID: 487448424} + - {fileID: 1420431041} + m_Father: {fileID: 771461370} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 150} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &868217821 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 868217819} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.HorizontalLayoutGroup + m_Padding: + m_Left: 0 + m_Right: 0 + m_Top: 0 + m_Bottom: 0 + m_ChildAlignment: 4 + m_Spacing: 100 + m_ChildForceExpandWidth: 1 + m_ChildForceExpandHeight: 1 + m_ChildControlWidth: 1 + m_ChildControlHeight: 1 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 + m_ReverseArrangement: 0 +--- !u!1 &942617858 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 942617859} + - component: {fileID: 942617861} + - component: {fileID: 942617860} + m_Layer: 0 + m_Name: Example_Player_No_Use + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &942617859 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 942617858} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &942617860 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 942617858} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5cf8d51844d0c6c49b1a9637b6d64bfe, type: 3} + m_Name: + m_EditorClassIdentifier: UUAVClient::UUAV.UUAVPlayer + url: https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_1MB.mp4 + playOnStart: 0 + targetTexture: {fileID: 0} + playerId: 0 + nativeChannels: 0 + videoSize: + Width: 0 + Height: 0 + enableDebugGather: 1 + currentTimeDebug: 0 + durationDebug: 0 + nativeState: + mediaInfo: + Duration: 0 + Framerate: 0 + VideoBitrate: 0 + AudioBitrate: 0 + Width: 0 + Height: 0 + SampleRate: 0 + Channels: 0 + HasVideo: 0 + HasAudio: 0 + VideoCodec: + PixelFormat: + AudioCodec: + SampleFormat: + nv12Material: {fileID: 0} + audioSource: {fileID: 0} + yPlane: {fileID: 0} + uvPlane: {fileID: 0} + runtimeSurface: {fileID: 0} +--- !u!82 &942617861 +AudioSource: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 942617858} + m_Enabled: 1 + serializedVersion: 4 + OutputAudioMixerGroup: {fileID: 0} + m_audioClip: {fileID: 0} + m_Resource: {fileID: 0} + m_PlayOnAwake: 1 + m_Volume: 1 + m_Pitch: 1 + Loop: 0 + Mute: 0 + Spatialize: 0 + SpatializePostEffects: 0 + Priority: 128 + DopplerLevel: 1 + MinDistance: 1 + MaxDistance: 500 + Pan2D: 0 + rolloffMode: 0 + BypassEffects: 0 + BypassListenerEffects: 0 + BypassReverbZones: 0 + rolloffCustomCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 1 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + panLevelCustomCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + spreadCustomCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + reverbZoneMixCustomCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 1 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 +--- !u!1 &1004410498 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1004410499} + - component: {fileID: 1004410502} + - component: {fileID: 1004410501} + - component: {fileID: 1004410500} + m_Layer: 5 + m_Name: InputField (TMP) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1004410499 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1004410498} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1190593601} + m_Father: {fileID: 602982405} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 0.5} + m_AnchorMax: {x: 1, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 1000, y: 100} + m_Pivot: {x: 1, y: 0.5} +--- !u!114 &1004410500 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1004410498} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2da0c512f12947e489f739169773d7ca, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TMP_InputField + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 1004410501} + m_TextViewport: {fileID: 1190593601} + m_TextComponent: {fileID: 1518043477} + m_Placeholder: {fileID: 327973007} + m_VerticalScrollbar: {fileID: 0} + m_VerticalScrollbarEventHandler: {fileID: 0} + m_LayoutGroup: {fileID: 0} + m_ScrollSensitivity: 1 + m_ContentType: 0 + m_InputType: 0 + m_AsteriskChar: 42 + m_KeyboardType: 0 + m_LineType: 0 + m_HideMobileInput: 0 + m_HideSoftKeyboard: 0 + m_CharacterValidation: 0 + m_RegexValue: + m_GlobalPointSize: 46 + m_CharacterLimit: 0 + m_OnEndEdit: + m_PersistentCalls: + m_Calls: [] + m_OnSubmit: + m_PersistentCalls: + m_Calls: [] + m_OnSelect: + m_PersistentCalls: + m_Calls: [] + m_OnDeselect: + m_PersistentCalls: + m_Calls: [] + m_OnTextSelection: + m_PersistentCalls: + m_Calls: [] + m_OnEndTextSelection: + m_PersistentCalls: + m_Calls: [] + m_OnValueChanged: + m_PersistentCalls: + m_Calls: [] + m_OnTouchScreenKeyboardStatusChanged: + m_PersistentCalls: + m_Calls: [] + m_CaretColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_CustomCaretColor: 0 + m_SelectionColor: {r: 0.65882355, g: 0.80784315, b: 1, a: 0.7529412} + m_Text: + m_CaretBlinkRate: 0.85 + m_CaretWidth: 1 + m_ReadOnly: 0 + m_RichText: 1 + m_GlobalFontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_OnFocusSelectAll: 1 + m_ResetOnDeActivation: 1 + m_KeepTextSelectionVisible: 0 + m_RestoreOriginalTextOnEscape: 1 + m_isRichTextEditingAllowed: 0 + m_LineLimit: 0 + isAlert: 0 + m_InputValidator: {fileID: 0} + m_ShouldActivateOnSelect: 1 +--- !u!114 &1004410501 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1004410498} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10911, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &1004410502 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1004410498} + m_CullTransparentMesh: 1 +--- !u!1 &1025554373 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1025554374} + - component: {fileID: 1025554376} + - component: {fileID: 1025554375} + m_Layer: 5 + m_Name: Checkmark + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1025554374 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1025554373} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1306935984} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 20, y: 20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1025554375 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1025554373} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10901, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &1025554376 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1025554373} + m_CullTransparentMesh: 1 +--- !u!1001 &1042744298 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 868217820} + m_Modifications: + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_Pivot.x + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_Pivot.y + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_SizeDelta.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalPosition.z + value: -0.549465 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3793433118894115418, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_text + value: 'Play + +' + objectReference: {fileID: 0} + - target: {fileID: 6434394354246174534, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_Name + value: Button Play + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} +--- !u!224 &1042744299 stripped +RectTransform: + m_CorrespondingSourceObject: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + m_PrefabInstance: {fileID: 1042744298} + m_PrefabAsset: {fileID: 0} +--- !u!114 &1042744300 stripped +MonoBehaviour: + m_CorrespondingSourceObject: {fileID: 2455051764364230330, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + m_PrefabInstance: {fileID: 1042744298} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Button +--- !u!1001 &1052056178 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 868217820} + m_Modifications: + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_Pivot.x + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_Pivot.y + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_SizeDelta.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalPosition.z + value: -0.549465 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3793433118894115418, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_text + value: 'Pause + +' + objectReference: {fileID: 0} + - target: {fileID: 6434394354246174534, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + propertyPath: m_Name + value: Button Pause + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} +--- !u!224 &1052056179 stripped +RectTransform: + m_CorrespondingSourceObject: {fileID: 2195277981447782603, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + m_PrefabInstance: {fileID: 1052056178} + m_PrefabAsset: {fileID: 0} +--- !u!114 &1052056180 stripped +MonoBehaviour: + m_CorrespondingSourceObject: {fileID: 2455051764364230330, guid: 0ea93280ccf09b24bb29d037adb0d0ab, type: 3} + m_PrefabInstance: {fileID: 1052056178} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Button +--- !u!1 &1190593600 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1190593601} + - component: {fileID: 1190593602} + m_Layer: 5 + m_Name: Text Area + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1190593601 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1190593600} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 327973005} + - {fileID: 1518043476} + m_Father: {fileID: 1004410499} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: -0.5} + m_SizeDelta: {x: -20, y: -13} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1190593602 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1190593600} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3312d7739989d2b4e91e6319e9a96d76, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.RectMask2D + m_Padding: {x: -8, y: -5, z: -8, w: -5} + m_Softness: {x: 0, y: 0} +--- !u!1 &1254537518 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1254537519} + - component: {fileID: 1254537521} + - component: {fileID: 1254537522} + m_Layer: 5 + m_Name: Label + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1254537519 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1254537518} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1420431041} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 9, y: -0.5} + m_SizeDelta: {x: -28, y: -3} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1254537521 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1254537518} + m_CullTransparentMesh: 1 +--- !u!114 &1254537522 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1254537518} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: Loop + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4294967295 + m_fontColor: {r: 1, g: 1, b: 1, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 36 + m_fontSizeBase: 36 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_characterHorizontalScale: 1 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 6e72656b + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!1 &1306935983 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1306935984} + - component: {fileID: 1306935986} + - component: {fileID: 1306935985} + m_Layer: 5 + m_Name: Background + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1306935984 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1306935983} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1025554374} + m_Father: {fileID: 1420431041} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0.5} + m_AnchorMax: {x: 0, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 80, y: 80} + m_Pivot: {x: 0, y: 0.5} +--- !u!114 &1306935985 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1306935983} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &1306935986 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1306935983} + m_CullTransparentMesh: 1 +--- !u!1 &1420431040 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1420431041} + - component: {fileID: 1420431042} + m_Layer: 5 + m_Name: Toggle + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1420431041 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1420431040} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1306935984} + - {fileID: 1254537519} + m_Father: {fileID: 868217820} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1420431042 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1420431040} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9085046f02f69544eb97fd06b6048fe2, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Toggle + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 1306935985} + toggleTransition: 1 + graphic: {fileID: 1025554375} + m_Group: {fileID: 0} + onValueChanged: + m_PersistentCalls: + m_Calls: [] + m_IsOn: 1 +--- !u!1 &1518043475 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1518043476} + - component: {fileID: 1518043478} + - component: {fileID: 1518043477} + m_Layer: 5 + m_Name: Text + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1518043476 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1518043475} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1190593601} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1518043477 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1518043475} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: "\u200B" + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4281479730 + m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 46 + m_fontSizeBase: 46 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 1 + m_VerticalAlignment: 256 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_characterHorizontalScale: 1 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 3 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 6e72656b + m_enableExtraPadding: 1 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!222 &1518043478 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1518043475} + m_CullTransparentMesh: 1 +--- !u!1 &1525984650 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1525984651} + - component: {fileID: 1525984653} + - component: {fileID: 1525984652} + m_Layer: 5 + m_Name: Text (TMP) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1525984651 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1525984650} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 602982405} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 475.5325, y: -50} + m_SizeDelta: {x: 951.065, y: 100} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1525984652 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1525984650} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: 'Status + +' + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4294967295 + m_fontColor: {r: 1, g: 1, b: 1, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 36 + m_fontSizeBase: 36 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_characterHorizontalScale: 1 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 6e72656b + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!222 &1525984653 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1525984650} + m_CullTransparentMesh: 1 +--- !u!1 &1681188490 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1681188493} + - component: {fileID: 1681188492} + - component: {fileID: 1681188491} + m_Layer: 0 + m_Name: EventSystem + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1681188491 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1681188490} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 01614664b831546d2ae94a42149d80ac, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.InputSystem::UnityEngine.InputSystem.UI.InputSystemUIInputModule + m_SendPointerHoverToParent: 1 + m_MoveRepeatDelay: 0.5 + m_MoveRepeatRate: 0.1 + m_XRTrackingOrigin: {fileID: 0} + m_ActionsAsset: {fileID: -944628639613478452, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_PointAction: {fileID: -1654692200621890270, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_MoveAction: {fileID: -8784545083839296357, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_SubmitAction: {fileID: 392368643174621059, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_CancelAction: {fileID: 7727032971491509709, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_LeftClickAction: {fileID: 3001919216989983466, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_MiddleClickAction: {fileID: -2185481485913320682, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_RightClickAction: {fileID: -4090225696740746782, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_ScrollWheelAction: {fileID: 6240969308177333660, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_TrackedDevicePositionAction: {fileID: 6564999863303420839, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_TrackedDeviceOrientationAction: {fileID: 7970375526676320489, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_DeselectOnBackgroundClick: 1 + m_PointerBehavior: 0 + m_CursorLockBehavior: 0 + m_ScrollDeltaPerTick: 6 +--- !u!114 &1681188492 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1681188490} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.EventSystems.EventSystem + m_FirstSelected: {fileID: 0} + m_sendNavigationEvents: 1 + m_DragThreshold: 10 +--- !u!4 &1681188493 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1681188490} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 330585546} + - {fileID: 410087041} + - {fileID: 832575519} + - {fileID: 942617859} + - {fileID: 771461370} + - {fileID: 1681188493} diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example/UUAVExample.unity.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example/UUAVExample.unity.meta new file mode 100644 index 00000000000..ed1ef901904 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Example/UUAVExample.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: bf7e6efdc36372f4587c5fa1261e0833 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime.meta new file mode 100644 index 00000000000..7f8560b5f60 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 850a4cc93694c374cb8569aff8443cfd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/NativeMethods.cs b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/NativeMethods.cs new file mode 100644 index 00000000000..7e56a856ce4 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/NativeMethods.cs @@ -0,0 +1,456 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; +using UnityEngine; + +namespace UUAV +{ + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void ErrorCallback(IntPtr message); + + // FFmpeg log sinks: one already-formatted, NUL-terminated line per call. + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void LogCallback(IntPtr message); + + // Mirrors FFmpeg's lavu_log_constants; cast to int at the FFI boundary. + public enum UUAVLogLevel + { + Quiet = -8, + Panic = 0, + Fatal = 8, + Error = 16, + Warning = 24, + Info = 32, + Verbose = 40, + Debug = 48, + Trace = 56, + } + + public enum UUAVState + { + Closed, + Opening, + Ready, + Playing, + Paused, + Ended, + Error, + Unknown, + } + + public static class UUAVStateExtensions + { + public static string ToStringNoAlloc(this UUAVState state) + { + return state switch + { + UUAVState.Closed => "Closed", + UUAVState.Opening => "Opening", + UUAVState.Ready => "Ready", + UUAVState.Playing => "Playing", + UUAVState.Paused => "Paused", + UUAVState.Ended => "Ended", + UUAVState.Error => "Error", + UUAVState.Unknown => "Unknown", + _ => throw new ArgumentOutOfRangeException(nameof(state), state, null) + }; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct AudioOptions + { + public int SampleRate; + public int Channels; + + public static AudioOptions FromConfig(AudioConfiguration config) + { + return new AudioOptions + { + SampleRate = config.sampleRate, + Channels = ChannelCount(config.speakerMode), + }; + } + + private static int ChannelCount(AudioSpeakerMode mode) + { + return mode switch + { + AudioSpeakerMode.Mono => 1, + AudioSpeakerMode.Stereo => 2, + AudioSpeakerMode.Quad => 4, + AudioSpeakerMode.Surround => 5, + AudioSpeakerMode.Mode5point1 => 6, + AudioSpeakerMode.Mode7point1 => 8, + _ => 2 + }; + } + } + + [StructLayout(LayoutKind.Sequential)] + [Serializable] + public struct VideoSize + { + public uint Width; + public uint Height; + } + + // Snapshot of the open media's source stream parameters, captured at + // open time. Name fields are NUL-terminated UTF-8; unknown values are + // 0 / empty, Duration is -1 for realtime streams. Video fields are + // zero when HasVideo is false, audio fields when HasAudio is false. + [StructLayout(LayoutKind.Sequential)] + public unsafe struct MediaInfo + { + private const int NameLen = 32; + + public double Duration; + public double Framerate; + public long VideoBitrate; + public long AudioBitrate; + public uint Width; + public uint Height; + public int SampleRate; + public int Channels; + private fixed byte videoCodec[NameLen]; + private fixed byte pixelFormat[NameLen]; + private fixed byte audioCodec[NameLen]; + private fixed byte sampleFormat[NameLen]; + private readonly byte hasVideo; + private readonly byte hasAudio; + + public bool HasVideo => hasVideo != 0; + + public bool HasAudio => hasAudio != 0; + + // e.g. "h264" + public string VideoCodec + { + get + { + fixed (byte* p = videoCodec) + { + return Utf8.FixedToString(p, NameLen); + } + } + } + + // e.g. "yuv420p" + public string PixelFormat + { + get + { + fixed (byte* p = pixelFormat) + { + return Utf8.FixedToString(p, NameLen); + } + } + } + + // e.g. "aac" + public string AudioCodec + { + get + { + fixed (byte* p = audioCodec) + { + return Utf8.FixedToString(p, NameLen); + } + } + } + + // e.g. "fltp" + public string SampleFormat + { + get + { + fixed (byte* p = sampleFormat) + { + return Utf8.FixedToString(p, NameLen); + } + } + } + } + + // Snapshot of the user-facing control values: the latest pushed + // intents, which the playback thread applies asynchronously. A + // *Pending flag means that control's latest push has not been + // consumed by the playback thread yet. + [StructLayout(LayoutKind.Sequential)] + public struct ControlsState + { + public double Rate; + private readonly byte play; + private readonly byte playPending; + private readonly byte looping; + private readonly byte loopingPending; + private readonly byte ratePending; + + public bool Play => play != 0; + public bool PlayPending => playPending != 0; + public bool Looping => looping != 0; + public bool LoopingPending => loopingPending != 0; + public bool RatePending => ratePending != 0; + } + + [StructLayout(LayoutKind.Sequential)] + public struct Status + { + public ulong PlayersCount; + private readonly byte initialized; + public AudioOptions AudioOptions; + + // allocated by the native side; release via ConsumeError + public IntPtr DeviceRemoveReason; + + public bool Initialized => initialized != 0; + + public string? ConsumeDeviceRemoveReason() + { + return Utf8.ConsumeCString(ref DeviceRemoveReason); + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct NewPlayerResult + { + public ulong PlayerId; + + // allocated by the native side; release via ConsumeError + public IntPtr ErrorMessage; + + public bool IsOk => ErrorMessage == IntPtr.Zero; + + // reads the message and frees it on the native side; call at most once + public string? ConsumeError() + { + return Utf8.ConsumeCString(ref ErrorMessage); + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct ResultFFI + { + // allocated by the native side; release via ConsumeError + public IntPtr ErrorMessage; + + public bool IsOk => ErrorMessage == IntPtr.Zero; + + // reads the message and frees it on the native side; call at most once + public string? ConsumeError() + { + return Utf8.ConsumeCString(ref ErrorMessage); + } + } + + internal static class NativeMethods + { + private const string Lib = "uuav"; + + // must be called exactly once per non-null message + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern void uuav_string_free(IntPtr str); + + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr uuav_abi_version(); + + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern ResultFFI uuav_init( + IntPtr probe_texture, + AudioOptions audioOptions, + ErrorCallback? errorCallback, + LogCallback? warningCallback, + LogCallback? logCallback, + [MarshalAs(UnmanagedType.LPUTF8Str)] string protocolWhitelist, + int logLevel + ); + + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern void uuav_set_log_level(int level); + + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern void uuav_deinit(); + + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern ResultFFI uuav_update_audio_out(AudioOptions options); + + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern Status uuav_status(); + + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern NewPlayerResult uuav_player_new(); + + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern void uuav_player_free(ulong playerId); + + // ---- lifecycle ------------------------------------------------- + + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern ResultFFI uuav_player_play(ulong playerId); + + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern ResultFFI uuav_player_pause(ulong playerId); + + // async! returns immediately + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + private static extern ResultFFI uuav_player_open_media_async(ulong playerId, IntPtr url); + + public static ResultFFI uuav_player_open_media_async(ulong playerId, string url) + { + var urlPtr = Utf8.AllocCString(url); + try + { + return uuav_player_open_media_async(playerId, urlPtr); + } + finally + { + Marshal.FreeHGlobal(urlPtr); + } + } + + // back to CLOSED, player reusable + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern ResultFFI uuav_player_close_media(ulong playerId); + + // ---- state ----------------------------------------------------- + + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern UUAVState uuav_player_state(ulong playerId); + + // valid from READY; may be unavailable for realtime streams + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern ResultFFI uuav_player_duration(ulong playerId, out double duration); + + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern ResultFFI uuav_player_current_time(ulong playerId, out double time); + + // the player slaves its playback to the externally provided master clock + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern ResultFFI uuav_player_assign_master_clock( + ulong playerId, + double currentTime + ); + + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern ResultFFI uuav_player_get_video_size( + ulong playerId, + out VideoSize size + ); + + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern ResultFFI uuav_player_get_media_info( + ulong playerId, + out MediaInfo info + ); + + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern ResultFFI uuav_player_current_controls_state( + ulong playerId, + out ControlsState state + ); + + // ---- transport (commands: set flags, decoder thread obeys) ------ + + // async; coalesces repeated calls + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern ResultFFI uuav_player_seek_async(ulong playerId, double time); + + // persists across url switches + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern ResultFFI uuav_player_set_looping( + ulong playerId, + [MarshalAs(UnmanagedType.U1)] bool looping + ); + + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.U1)] + public static extern bool uuav_player_get_looping(ulong playerId); + + // Not applicable for realtime streams + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern ResultFFI uuav_player_set_rate(ulong playerId, double rate); + + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern double uuav_player_get_rate(ulong playerId); + + // ---- video ------------------------------------------------------- + + // pass to GL.IssuePluginEvent / CommandBuffer.IssuePluginEvent + // with the player id as the event id + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr uuav_get_render_callback(); + + // SRV: plane 0 = Y, 1 = UV; valid from READY; recreated on resolution change + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern ResultFFI uuav_player_get_video_texture( + ulong playerId, + int plane, + out IntPtr texture + ); + + // ---- audio ------------------------------------------------------- + + // [audio] fills interleaved FLT; pads silence on underrun, + // never blocks; returns frames actually copied + [DllImport(Lib, CallingConvention = CallingConvention.Cdecl)] + public static extern int uuav_player_read_audio( + ulong playerId, + [Out] float[] dst, + int nbFrames + ); + } + + internal static class Utf8 + { + public static unsafe string? PtrToString(IntPtr ptr) + { + if (ptr == IntPtr.Zero) + { + return null; + } + + var bytes = (byte*)ptr; + var length = 0; + while (bytes[length] != 0) + { + length++; + } + + return Encoding.UTF8.GetString(bytes, length); + } + + // reads a NUL-terminated UTF-8 string from a fixed-size buffer + public static unsafe string FixedToString(byte* buffer, int capacity) + { + var length = 0; + while (length < capacity && buffer[length] != 0) + { + length++; + } + + return Encoding.UTF8.GetString(buffer, length); + } + + public static IntPtr AllocCString(string value) + { + var bytes = Encoding.UTF8.GetBytes(value); + var ptr = Marshal.AllocHGlobal(bytes.Length + 1); + Marshal.Copy(bytes, 0, ptr, bytes.Length); + Marshal.WriteByte(ptr, bytes.Length, 0); + return ptr; + } + + public static string? ConsumeCString(ref IntPtr ptr) + { + if (ptr == IntPtr.Zero) + { + return null; + } + + var message = Utf8.PtrToString(ptr); + NativeMethods.uuav_string_free(ptr); + ptr = IntPtr.Zero; + return message; + } + } +} diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/NativeMethods.cs.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/NativeMethods.cs.meta new file mode 100644 index 00000000000..71d53839ae0 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/NativeMethods.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6dfdd3c407e66344ba47c00c7dec9c82 \ No newline at end of file diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins.meta new file mode 100644 index 00000000000..b9574739f08 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 72865f9a47ece0b4dab850834319c93d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS.meta new file mode 100644 index 00000000000..0f9b2dd856f --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8cd238d39f754189a757d35503d863e4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavcodec.62.dylib b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavcodec.62.dylib new file mode 100755 index 00000000000..724fecc505d Binary files /dev/null and b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavcodec.62.dylib differ diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavcodec.62.dylib.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavcodec.62.dylib.meta new file mode 100644 index 00000000000..e81a47c831c --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavcodec.62.dylib.meta @@ -0,0 +1,46 @@ +fileFormatVersion: 2 +guid: 9d5716c3ccd446028e6499db15a7126c +PluginImporter: + externalObjects: {} + serializedVersion: 3 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + Any: + enabled: 0 + settings: + Exclude Editor: 0 + Exclude Linux64: 1 + Exclude OSXUniversal: 0 + Exclude Win: 1 + Exclude Win64: 1 + Editor: + enabled: 1 + settings: + CPU: ARM64 + DefaultValueInitialized: true + OS: OSX + Linux64: + enabled: 0 + settings: + CPU: x86_64 + OSXUniversal: + enabled: 1 + settings: + CPU: ARM64 + Win: + enabled: 0 + settings: + CPU: x86 + Win64: + enabled: 0 + settings: + CPU: None + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavdevice.62.dylib b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavdevice.62.dylib new file mode 100755 index 00000000000..df6ebea3187 Binary files /dev/null and b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavdevice.62.dylib differ diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavdevice.62.dylib.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavdevice.62.dylib.meta new file mode 100644 index 00000000000..d10d5ce55a6 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavdevice.62.dylib.meta @@ -0,0 +1,46 @@ +fileFormatVersion: 2 +guid: 1bbfd5dac71841c39acf49fab8a762e8 +PluginImporter: + externalObjects: {} + serializedVersion: 3 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + Any: + enabled: 0 + settings: + Exclude Editor: 0 + Exclude Linux64: 1 + Exclude OSXUniversal: 0 + Exclude Win: 1 + Exclude Win64: 1 + Editor: + enabled: 1 + settings: + CPU: ARM64 + DefaultValueInitialized: true + OS: OSX + Linux64: + enabled: 0 + settings: + CPU: x86_64 + OSXUniversal: + enabled: 1 + settings: + CPU: ARM64 + Win: + enabled: 0 + settings: + CPU: x86 + Win64: + enabled: 0 + settings: + CPU: None + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavfilter.11.dylib b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavfilter.11.dylib new file mode 100755 index 00000000000..e6be60b3e1d Binary files /dev/null and b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavfilter.11.dylib differ diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavfilter.11.dylib.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavfilter.11.dylib.meta new file mode 100644 index 00000000000..07fc2dd1f3a --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavfilter.11.dylib.meta @@ -0,0 +1,46 @@ +fileFormatVersion: 2 +guid: 748da73f907b4442aa335f0a9fcd76d0 +PluginImporter: + externalObjects: {} + serializedVersion: 3 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + Any: + enabled: 0 + settings: + Exclude Editor: 0 + Exclude Linux64: 1 + Exclude OSXUniversal: 0 + Exclude Win: 1 + Exclude Win64: 1 + Editor: + enabled: 1 + settings: + CPU: ARM64 + DefaultValueInitialized: true + OS: OSX + Linux64: + enabled: 0 + settings: + CPU: x86_64 + OSXUniversal: + enabled: 1 + settings: + CPU: ARM64 + Win: + enabled: 0 + settings: + CPU: x86 + Win64: + enabled: 0 + settings: + CPU: None + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavformat.62.dylib b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavformat.62.dylib new file mode 100755 index 00000000000..4cc60532be3 Binary files /dev/null and b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavformat.62.dylib differ diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavformat.62.dylib.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavformat.62.dylib.meta new file mode 100644 index 00000000000..49f0c0792d7 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavformat.62.dylib.meta @@ -0,0 +1,46 @@ +fileFormatVersion: 2 +guid: 5e65434492c240788bf2e83ed34df894 +PluginImporter: + externalObjects: {} + serializedVersion: 3 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + Any: + enabled: 0 + settings: + Exclude Editor: 0 + Exclude Linux64: 1 + Exclude OSXUniversal: 0 + Exclude Win: 1 + Exclude Win64: 1 + Editor: + enabled: 1 + settings: + CPU: ARM64 + DefaultValueInitialized: true + OS: OSX + Linux64: + enabled: 0 + settings: + CPU: x86_64 + OSXUniversal: + enabled: 1 + settings: + CPU: ARM64 + Win: + enabled: 0 + settings: + CPU: x86 + Win64: + enabled: 0 + settings: + CPU: None + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavutil.60.dylib b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavutil.60.dylib new file mode 100755 index 00000000000..6fa242366c3 Binary files /dev/null and b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavutil.60.dylib differ diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavutil.60.dylib.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavutil.60.dylib.meta new file mode 100644 index 00000000000..6493904da9b --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libavutil.60.dylib.meta @@ -0,0 +1,46 @@ +fileFormatVersion: 2 +guid: 91ef2031a470442b8ca1f965af024d46 +PluginImporter: + externalObjects: {} + serializedVersion: 3 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + Any: + enabled: 0 + settings: + Exclude Editor: 0 + Exclude Linux64: 1 + Exclude OSXUniversal: 0 + Exclude Win: 1 + Exclude Win64: 1 + Editor: + enabled: 1 + settings: + CPU: ARM64 + DefaultValueInitialized: true + OS: OSX + Linux64: + enabled: 0 + settings: + CPU: x86_64 + OSXUniversal: + enabled: 1 + settings: + CPU: ARM64 + Win: + enabled: 0 + settings: + CPU: x86 + Win64: + enabled: 0 + settings: + CPU: None + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libswresample.6.dylib b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libswresample.6.dylib new file mode 100755 index 00000000000..4c2ba9d6c41 Binary files /dev/null and b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libswresample.6.dylib differ diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libswresample.6.dylib.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libswresample.6.dylib.meta new file mode 100644 index 00000000000..55355ac0b47 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libswresample.6.dylib.meta @@ -0,0 +1,46 @@ +fileFormatVersion: 2 +guid: 1ffb11e415384543861d25ab0fc31146 +PluginImporter: + externalObjects: {} + serializedVersion: 3 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + Any: + enabled: 0 + settings: + Exclude Editor: 0 + Exclude Linux64: 1 + Exclude OSXUniversal: 0 + Exclude Win: 1 + Exclude Win64: 1 + Editor: + enabled: 1 + settings: + CPU: ARM64 + DefaultValueInitialized: true + OS: OSX + Linux64: + enabled: 0 + settings: + CPU: x86_64 + OSXUniversal: + enabled: 1 + settings: + CPU: ARM64 + Win: + enabled: 0 + settings: + CPU: x86 + Win64: + enabled: 0 + settings: + CPU: None + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libswscale.9.dylib b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libswscale.9.dylib new file mode 100755 index 00000000000..f6c56d649db Binary files /dev/null and b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libswscale.9.dylib differ diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libswscale.9.dylib.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libswscale.9.dylib.meta new file mode 100644 index 00000000000..17e421c449c --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libswscale.9.dylib.meta @@ -0,0 +1,46 @@ +fileFormatVersion: 2 +guid: b6a07397e2a14975ae52c2c6b5ac9bcb +PluginImporter: + externalObjects: {} + serializedVersion: 3 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + Any: + enabled: 0 + settings: + Exclude Editor: 0 + Exclude Linux64: 1 + Exclude OSXUniversal: 0 + Exclude Win: 1 + Exclude Win64: 1 + Editor: + enabled: 1 + settings: + CPU: ARM64 + DefaultValueInitialized: true + OS: OSX + Linux64: + enabled: 0 + settings: + CPU: x86_64 + OSXUniversal: + enabled: 1 + settings: + CPU: ARM64 + Win: + enabled: 0 + settings: + CPU: x86 + Win64: + enabled: 0 + settings: + CPU: None + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libuuav.dylib b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libuuav.dylib new file mode 100755 index 00000000000..371fa93defe Binary files /dev/null and b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libuuav.dylib differ diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libuuav.dylib.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libuuav.dylib.meta new file mode 100644 index 00000000000..e7e912d81a0 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/macOS/libuuav.dylib.meta @@ -0,0 +1,46 @@ +fileFormatVersion: 2 +guid: 8e40407d24f24de7aa93ecbfbd8fb513 +PluginImporter: + externalObjects: {} + serializedVersion: 3 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + Any: + enabled: 0 + settings: + Exclude Editor: 0 + Exclude Linux64: 1 + Exclude OSXUniversal: 0 + Exclude Win: 1 + Exclude Win64: 1 + Editor: + enabled: 1 + settings: + CPU: ARM64 + DefaultValueInitialized: true + OS: OSX + Linux64: + enabled: 0 + settings: + CPU: x86_64 + OSXUniversal: + enabled: 1 + settings: + CPU: ARM64 + Win: + enabled: 0 + settings: + CPU: x86 + Win64: + enabled: 0 + settings: + CPU: None + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64.meta new file mode 100644 index 00000000000..d3e1e06101f --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6b998b588ca873b42a6a06e8c39a5258 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avcodec-63.dll b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avcodec-63.dll new file mode 100644 index 00000000000..2c9aa3c9458 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avcodec-63.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0b0ce2e88f4c703d96fe69f741e93bf04f96ca456239ceaec9beebfa59dbe35 +size 18923520 diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avcodec-63.dll.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avcodec-63.dll.meta new file mode 100644 index 00000000000..2083831f7cd --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avcodec-63.dll.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 60aa1c80c50208440942d0ac43fd5722 \ No newline at end of file diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avdevice-63.dll b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avdevice-63.dll new file mode 100644 index 00000000000..81726c986b5 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avdevice-63.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b95c1f660ebdae93f150a0c94fc94c2534be451bdf92b5c0c24614fa97d4e844 +size 155136 diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avdevice-63.dll.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avdevice-63.dll.meta new file mode 100644 index 00000000000..067a24ebf61 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avdevice-63.dll.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ff5a50fb17101d84985b9a158b0a225f \ No newline at end of file diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avfilter-12.dll b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avfilter-12.dll new file mode 100644 index 00000000000..e561787f81d --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avfilter-12.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a66a569884ae7b6d43d483f99a3602b5475472fcc9206646ed73588c90fb81e3 +size 5823488 diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avfilter-12.dll.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avfilter-12.dll.meta new file mode 100644 index 00000000000..04eabbe297e --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avfilter-12.dll.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2c96a2c38b8dc0d43817f6b88439160d \ No newline at end of file diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avformat-63.dll b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avformat-63.dll new file mode 100644 index 00000000000..db099a14db4 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avformat-63.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e29c82de9d96a82bed3b52c89cd55c76e02fc557dab0c2c87df7e574d77078f3 +size 2885120 diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avformat-63.dll.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avformat-63.dll.meta new file mode 100644 index 00000000000..f636c3891ea --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avformat-63.dll.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1170c4652b9af714294b0fe77056360c \ No newline at end of file diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avutil-61.dll b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avutil-61.dll new file mode 100644 index 00000000000..ab0dfd290d4 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avutil-61.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d870187f5a2a13d5cd7b61adb1bd22681d2d3520268f7f99b900746d445dd1c4 +size 1105920 diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avutil-61.dll.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avutil-61.dll.meta new file mode 100644 index 00000000000..90ce79e7e9d --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/avutil-61.dll.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 894909d6e6e8ce14588142b52f56f0e6 \ No newline at end of file diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/libbz2-1.dll b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/libbz2-1.dll new file mode 100644 index 00000000000..332440012fc --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/libbz2-1.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b79cd3a7102e359ef6324e98966a67939e58ec2b651ae2847cae84a4dfa453f7 +size 100964 diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/libbz2-1.dll.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/libbz2-1.dll.meta new file mode 100644 index 00000000000..db26d903a79 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/libbz2-1.dll.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 17290e62cde13474f8d53052310fb981 \ No newline at end of file diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/libiconv-2.dll b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/libiconv-2.dll new file mode 100644 index 00000000000..34ce331888d --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/libiconv-2.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a282a854e01be726c6cccfe46f548c716aa45b3014818468253aaa4efbcd067 +size 1143148 diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/libiconv-2.dll.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/libiconv-2.dll.meta new file mode 100644 index 00000000000..a5fe1900ed4 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/libiconv-2.dll.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: da123183d95a81944835730fb4dfe0b5 \ No newline at end of file diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/liblzma-5.dll b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/liblzma-5.dll new file mode 100644 index 00000000000..bcb8c3c272d --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/liblzma-5.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc9edb9bd83b0371fe68d96cb3e70cd17b7a5aba958326a18f8210cb812432e9 +size 191836 diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/liblzma-5.dll.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/liblzma-5.dll.meta new file mode 100644 index 00000000000..e980e1ff19a --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/liblzma-5.dll.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 47c34e625f1ad9d42a064753f8135481 \ No newline at end of file diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/libwinpthread-1.dll b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/libwinpthread-1.dll new file mode 100644 index 00000000000..b4a22d9a57e --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/libwinpthread-1.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bbd639c9be4bc7517beb0fac9ac13502d270e9bf07fcd223de8cc40d9c15408 +size 66467 diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/libwinpthread-1.dll.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/libwinpthread-1.dll.meta new file mode 100644 index 00000000000..62aaef18626 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/libwinpthread-1.dll.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 88f059668a184974e94972e218a902b9 \ No newline at end of file diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/swresample-7.dll b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/swresample-7.dll new file mode 100644 index 00000000000..90cc600e7a7 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/swresample-7.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0cd5b74734778619534b8d4f51c55a5c5fd95e798cde9b8c23dc1a5a215006a3 +size 179712 diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/swresample-7.dll.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/swresample-7.dll.meta new file mode 100644 index 00000000000..e66744f0347 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/swresample-7.dll.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3313434662d8550458fd9969679553a3 \ No newline at end of file diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/swscale-10.dll b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/swscale-10.dll new file mode 100644 index 00000000000..9f89d82abb5 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/swscale-10.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05f5e9924748fbe782af8388ce9c3afb7e30b7a77d87158eaab009d5240fe9f2 +size 2122240 diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/swscale-10.dll.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/swscale-10.dll.meta new file mode 100644 index 00000000000..48e2d15174c --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/swscale-10.dll.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5e913440c805b394db207b2c00a2aefd \ No newline at end of file diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/uuav.dll b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/uuav.dll new file mode 100644 index 00000000000..f65bd4959ce --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/uuav.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31420a413e4a23fc0328a0184e996cd11d37e3303c0c06a6f1515b341afda2ad +size 1796254 diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/uuav.dll.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/uuav.dll.meta new file mode 100644 index 00000000000..688b6534c9c --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/uuav.dll.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: dc71e5d1c720b3c428964b8723df9e77 \ No newline at end of file diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/zlib1.dll b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/zlib1.dll new file mode 100644 index 00000000000..33423b112d3 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/zlib1.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb7ab3788d10940df874acd97b1821bbb5ee4a91f3eec11982bb5bf7a3c96443 +size 120814 diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/zlib1.dll.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/zlib1.dll.meta new file mode 100644 index 00000000000..a8bfcfef3f5 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Plugins/x86_64/zlib1.dll.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: cc6000e424770d349a7401758f79b659 \ No newline at end of file diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Shaders.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Shaders.meta new file mode 100644 index 00000000000..162d4582366 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Shaders.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: cd42192cb1969f64087415a5e18bef9e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Shaders/NV12ToRGB.shader b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Shaders/NV12ToRGB.shader new file mode 100644 index 00000000000..f35564ba352 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Shaders/NV12ToRGB.shader @@ -0,0 +1,49 @@ +Shader "Hidden/UUAV/NV12ToRGB" +{ + Properties + { + _YTex ("Y plane", 2D) = "black" {} + _UVTex ("UV plane", 2D) = "gray" {} + } + SubShader + { + Pass + { + ZTest Always + ZWrite Off + Cull Off + + CGPROGRAM + #pragma vertex vert_img + #pragma fragment frag + #include "UnityCG.cginc" + + sampler2D _YTex; + sampler2D _UVTex; + + fixed4 frag(v2f_img i) : SV_Target + { + // video rows are top-down, Unity UVs are bottom-up + float2 uv = float2(i.uv.x, 1.0 - i.uv.y); + + // BT.709 limited range + // TODO select BT.601 / full range once native exports colorimetry + float y = 1.1643835 * (tex2D(_YTex, uv).r - 0.0627451); + float2 c = tex2D(_UVTex, uv).rg - 0.5; // r = Cb, g = Cr + float3 rgb = saturate(float3( + y + 1.7927411 * c.y, + y - 0.2132486 * c.x - 0.5329093 * c.y, + y + 2.1124018 * c.x)); + // video RGB is display-referred (BT.709 OETF ~ sRGB); the + // linear pipeline expects scene-linear samples, the sRGB + // render target re-encodes on store + #ifndef UNITY_COLORSPACE_GAMMA + rgb = GammaToLinearSpace(rgb); + #endif + return fixed4(rgb, 1.0); + } + ENDCG + } + } + Fallback Off +} diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Shaders/NV12ToRGB.shader.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Shaders/NV12ToRGB.shader.meta new file mode 100644 index 00000000000..1d1658c08e6 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/Shaders/NV12ToRGB.shader.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 7c27dfc00496c5442a73aeede131877d +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/UUAVClient.asmdef b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/UUAVClient.asmdef new file mode 100644 index 00000000000..d3696939fda --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/UUAVClient.asmdef @@ -0,0 +1,14 @@ +{ + "name": "UUAVClient", + "rootNamespace": "UUAVClient", + "references": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/UUAVClient.asmdef.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/UUAVClient.asmdef.meta new file mode 100644 index 00000000000..686a8437ff5 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/UUAVClient.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d9709898aa920584a8d1613950e6a7f5 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/UUAVPlayer.cs b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/UUAVPlayer.cs new file mode 100644 index 00000000000..99061d742f2 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/UUAVPlayer.cs @@ -0,0 +1,524 @@ +using System; +using UnityEngine; + +namespace UUAV +{ + [RequireComponent(typeof(AudioSource))] + public sealed class UUAVPlayer : MonoBehaviour + { + private delegate ResultFFI TimeQuery(ulong playerId, out double value); + + // Cache to avoid per-call allocs + private static readonly TimeQuery currentTimeQuery = NativeMethods.uuav_player_current_time; + private static readonly TimeQuery durationQuery = NativeMethods.uuav_player_duration; + + /// + /// M stands for "Managed" mirrors the original MediaInfo and makes it serializable + /// + [Serializable] + public struct MediaInfo_M + { + public double Duration; + public double Framerate; + public long VideoBitrate; + public long AudioBitrate; + public uint Width; + public uint Height; + public int SampleRate; + public int Channels; + public bool HasVideo; + + public bool HasAudio; + + // e.g. "h264" + public string VideoCodec; + + // e.g. "yuv420p" + public string PixelFormat; + + // e.g. "aac" + public string AudioCodec; + + // e.g. "fltp" + public string SampleFormat; + + public static MediaInfo_M From(MediaInfo mediaInfo) + { + return new MediaInfo_M + { + Duration = mediaInfo.Duration, + Framerate = mediaInfo.Framerate, + VideoBitrate = mediaInfo.VideoBitrate, + AudioBitrate = mediaInfo.AudioBitrate, + Width = mediaInfo.Width, + Height = mediaInfo.Height, + SampleRate = mediaInfo.SampleRate, + Channels = mediaInfo.Channels, + HasVideo = mediaInfo.HasVideo, + HasAudio = mediaInfo.HasAudio, + VideoCodec = mediaInfo.VideoCodec, + PixelFormat = mediaInfo.PixelFormat, + AudioCodec = mediaInfo.AudioCodec, + SampleFormat = mediaInfo.SampleFormat + }; + } + } + + private static readonly int YTexId = Shader.PropertyToID("_YTex"); + private static readonly int UVTexId = Shader.PropertyToID("_UVTex"); + + [SerializeField] private string url = ""; + [SerializeField] private bool playOnStart; + + // optional user-provided surface; auto-allocated at video size when null + [SerializeField] private RenderTexture? targetTexture; + + [Header("Debug View")] + // immutable after Awake; 0 means creation failed and the component is inert + [SerializeField] + private ulong playerId; + + [SerializeField] private int nativeChannels; + [SerializeField] private VideoSize videoSize; + + [SerializeField] private bool enableDebugGather; + [SerializeField] private double currentTimeDebug; + [SerializeField] private double durationDebug; + [SerializeField] private string? nativeState; + [SerializeField] private MediaInfo_M mediaInfo; + + [Header("Resources")] [SerializeField] private Material? nv12Material; + [SerializeField] private AudioSource audioSource = null!; + [SerializeField] private Texture2D? yPlane; + [SerializeField] private Texture2D? uvPlane; + [SerializeField] private RenderTexture? runtimeSurface; + [SerializeField] private IntPtr nativeTexture; + + private static ulong playerIncrementalID; + + public string CurrentUrl => url; + + public UUAVState State => NativeMethods.uuav_player_state(playerId); + + public double CurrentTime => ReadTime(currentTimeQuery); + + public double Duration => ReadTime(durationQuery); + + public bool Looping + { + get => NativeMethods.uuav_player_get_looping(playerId); + set => Check(NativeMethods.uuav_player_set_looping(playerId, value), "set looping"); + } + + public double PlaybackRate + { + get => NativeMethods.uuav_player_get_rate(playerId); + set => Check(NativeMethods.uuav_player_set_rate(playerId, value), "set rate"); + } + + public RenderTexture? CurrentTexture => + targetTexture != null ? targetTexture : runtimeSurface; + + public AudioSource AudioSource => audioSource; + + public void OpenMedia(string mediaUrl) + { + url = mediaUrl; + Check(NativeMethods.uuav_player_open_media_async(playerId, mediaUrl), "open media"); + } + + [ContextMenu(nameof(PrintState))] + private void PrintState() + { + Debug.Log($"[UUAV] state: {NativeMethods.uuav_player_state(playerId).ToStringNoAlloc()}"); + } + + [ContextMenu(nameof(PrintControlsState))] + private void PrintControlsState() + { + var result = NativeMethods.uuav_player_current_controls_state(playerId, out var controls); + if (result.IsOk == false) + { + Debug.LogError($"[UUAV] controls state: {result.ConsumeError()}"); + return; + } + + Debug.Log( + $"[UUAV] controls: play={controls.Play} (pending={controls.PlayPending}) " + + $"looping={controls.Looping} (pending={controls.LoopingPending}) " + + $"rate={controls.Rate} (pending={controls.RatePending})" + ); + } + + [ContextMenu(nameof(OpenMedia))] + private void OpenMedia() + { + OpenMedia(url); + } + + [ContextMenu(nameof(CloseMedia))] + public void CloseMedia() + { + Check(NativeMethods.uuav_player_close_media(playerId), "close media"); + } + + [ContextMenu(nameof(Play))] + public void Play() + { + Check(NativeMethods.uuav_player_play(playerId), "play"); + } + + [ContextMenu(nameof(Pause))] + public void Pause() + { + Check(NativeMethods.uuav_player_pause(playerId), "pause"); + } + + public void Seek(double time) + { + Check(NativeMethods.uuav_player_seek_async(playerId, time), "seek"); + } + + public bool TryGetMediaInfo(out MediaInfo info) + { + info = default; + if (playerId == 0) + { + return false; + } + + if (State is UUAVState.Closed or UUAVState.Error) + { + return false; + } + + var result = NativeMethods.uuav_player_get_media_info(playerId, out info); + Check(result, "uuav_player_get_media_info"); + return result.IsOk; + } + + // NOTE if long-session drift corrections become audible, derive media + // time from frames consumed in OnAudioFilterRead and feed that here + // instead of the caller's clock + public void AssignMasterClock(double mediaTime) + { + Check( + NativeMethods.uuav_player_assign_master_clock(playerId, mediaTime), + "assign master clock" + ); + } + + public static UUAVPlayer New() + { + ulong currentID = ++playerIncrementalID; + GameObject gm = new GameObject($"UUAVPlayer_Instance_{currentID}"); + UUAVPlayer instance = gm.AddComponent(); + return instance; + } + + private void Awake() + { + audioSource = GetComponent(); + + var shader = Shader.Find("Hidden/UUAV/NV12ToRGB"); + if (shader == null) + { + Debug.LogError("[UUAV] shader Hidden/UUAV/NV12ToRGB not found"); + return; + } + + nv12Material = new Material(shader); + + var newPlayer = NativeMethods.uuav_player_new(); + if (newPlayer.IsOk == false) + { + Debug.LogError($"[UUAV] new player: {newPlayer.ConsumeError()}"); + return; + } + + playerId = newPlayer.PlayerId; + nativeChannels = NativeMethods.uuav_status().AudioOptions.Channels; + + audioSource.Play(); + } + + private void Start() + { + if (playOnStart && url.Length > 0) + { + OpenMedia(url); + Play(); + } + } + + private void Update() + { + if (playerId == 0) + { + return; + } + + if (enableDebugGather) + { + durationDebug = Duration; + currentTimeDebug = CurrentTime; + nativeState = NativeMethods.uuav_player_state(playerId).ToStringNoAlloc(); + + if (TryGetMediaInfo(out MediaInfo m)) + { + mediaInfo = MediaInfo_M.From(m); + } + } + + switch (NativeMethods.uuav_player_state(playerId)) + { + case UUAVState.Ready: + case UUAVState.Playing: + case UUAVState.Paused: + case UUAVState.Ended: + break; + default: + return; + } + + // presents the due frame into the native NV12 texture on the render + // thread; the blit below is enqueued after it in submission order, + // so it samples the freshly presented frame in the same frame + GL.IssuePluginEvent(UUAVRuntime.RenderCallback, (int)playerId); + + if (RefreshVideoTexture()) + { + BlitToSurface(); + } + } + + private void OnDisable() + { + if (State == UUAVState.Playing) + { + Pause(); + } + } + + private void OnDestroy() + { + audioSource.Stop(); + + if (playerId != 0) + { + NativeMethods.uuav_player_free(playerId); + } + + ReleasePlaneViews(); + + if (runtimeSurface != null) + { + runtimeSurface.Release(); + Destroy(runtimeSurface); + runtimeSurface = null; + } + + if (nv12Material != null) + { + Destroy(nv12Material); + nv12Material = null; + } + } + + + // audio mixer thread. playerId is immutable, safe to read directly; + // a stale id after uuav_player_free is a native no-op returning 0 + private void OnAudioFilterRead(float[] data, int channels) + { + if (playerId == 0 || channels != nativeChannels) + { + // a channel mismatch means the engine's audio config changed; + // renegotiation is runtime-wide, not per player, + // emit silence rather than misinterleaved audio + Array.Clear(data, 0, data.Length); + return; + } + + var read = NativeMethods.uuav_player_read_audio(playerId, data, data.Length / channels); + if (read == 0) + { + // missing/freed player leaves the buffer untouched + Array.Clear(data, 0, data.Length); + } + } + + private bool RefreshVideoTexture() + { + var textureResult = NativeMethods.uuav_player_get_video_texture( + playerId, + 0, + out var yTexture + ); + if (textureResult.IsOk == false) + { + // expected until the first frame is presented + textureResult.ConsumeError(); + return false; + } + + var uvTexture = yTexture; + + // on Metal each plane is its own texture; both pointers change + // together on resolution change, so polling plane 0 covers both. + // on D3D11 native ignores the plane and returns the one resource +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX + var uvResult = NativeMethods.uuav_player_get_video_texture( + playerId, + 1, + out uvTexture + ); + if (uvResult.IsOk == false) + { + uvResult.ConsumeError(); + return false; + } +#endif + + var sizeResult = NativeMethods.uuav_player_get_video_size(playerId, out var size); + if (sizeResult.IsOk == false) + { + sizeResult.ConsumeError(); + return false; + } + + // the native texture is recreated on every resolution change, + // including mid-play adaptive/IDR switches, so pointer + size + // comparison catches first frame, re-open and mid-stream changes + if ( + yTexture != nativeTexture + || size.Width != videoSize.Width + || size.Height != videoSize.Height + ) + { + RecreatePlaneViews(yTexture, uvTexture, size); + } + + return yPlane != null; + } + + private void RecreatePlaneViews(IntPtr yTexture, IntPtr uvTexture, VideoSize size) + { + ReleasePlaneViews(); + + var width = (int)size.Width; + var height = (int)size.Height; + + // D3D11: one NV12 resource passed for both planes, Unity selects + // the plane from the view format (R8 -> Y, R8G8 -> UV). + // Metal: two distinct MTLTextures, wrapped as-is + yPlane = Texture2D.CreateExternalTexture( + width, + height, + TextureFormat.R8, + mipChain: false, + linear: true, + yTexture + ); + uvPlane = Texture2D.CreateExternalTexture( + width / 2, + height / 2, + TextureFormat.RG16, + mipChain: false, + linear: true, + uvTexture + ); + ConfigurePlane(yPlane); + ConfigurePlane(uvPlane); + + if (targetTexture == null) + { + if (runtimeSurface != null) + { + runtimeSurface.Release(); + Destroy(runtimeSurface); + } + + // BGRA8 group so the surface is Graphics.CopyTexture-compatible with + // BGRA render targets (the D3D11 output format of most video pipelines). + // Default read/write leaves the surface sRGB-flagged in Linear projects; + // CopyTexture destinations must be sRGB-flagged too, or the raw bit copy + // transplants gamma bytes that get sampled as linear (too bright) + runtimeSurface = new RenderTexture(width, height, 0, RenderTextureFormat.BGRA32); + } + + nativeTexture = yTexture; + videoSize = size; + } + + private void BlitToSurface() + { + var surface = CurrentTexture; + if (surface == null || nv12Material == null) + { + return; + } + + nv12Material.SetTexture(YTexId, yPlane); + nv12Material.SetTexture(UVTexId, uvPlane); + + // script-context blits inherit whatever sRGB-write state the last + // render left behind; without the encode the shader's linear output + // lands raw in the sRGB surface and decodes too dark + bool previousSRGBWrite = GL.sRGBWrite; + GL.sRGBWrite = QualitySettings.activeColorSpace == ColorSpace.Linear; + Graphics.Blit(null, surface, nv12Material); + GL.sRGBWrite = previousSRGBWrite; + } + + private void ReleasePlaneViews() + { + // wrappers only: Unity drops its SRVs, the native resource is untouched + if (yPlane != null) + { + Destroy(yPlane); + yPlane = null; + } + + if (uvPlane != null) + { + Destroy(uvPlane); + uvPlane = null; + } + + nativeTexture = IntPtr.Zero; + videoSize = default; + } + + private double ReadTime(TimeQuery query) + { + if (playerId == 0) + { + return 0; + } + + var result = query(playerId, out var value); + if (result.IsOk) + { + return value; + } + + // unavailable is expected (no media open / realtime stream) + result.ConsumeError(); + return 0; + } + + private static void Check(ResultFFI result, string operation) + { + if (result.IsOk == false) + { + Debug.LogError($"[UUAV] {operation}: {result.ConsumeError()}"); + } + } + + private static void ConfigurePlane(Texture2D plane) + { + plane.filterMode = FilterMode.Bilinear; + plane.wrapMode = TextureWrapMode.Clamp; + } + } +} diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/UUAVPlayer.cs.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/UUAVPlayer.cs.meta new file mode 100644 index 00000000000..2b05ad1517a --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/UUAVPlayer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5cf8d51844d0c6c49b1a9637b6d64bfe \ No newline at end of file diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/UUAVRuntime.cs b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/UUAVRuntime.cs new file mode 100644 index 00000000000..a080129ffaf --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/UUAVRuntime.cs @@ -0,0 +1,200 @@ +using System; +using System.Text; +using AOT; +using UnityEngine; + +namespace UUAV +{ + /// + /// Owns the native runtime lifecycle + /// + internal static class UUAVRuntime + { + // FFmpeg protocol_whitelist for production with untrusted sources. + // Media URLs come from untrusted scenes, so this omits file: and the nested-protocol pivots (concat, subfile, ...); + private const string UNTRUSTED_STREAMING_PROTOCOLS = "https,http,tls,tcp,crypto,data,udp,rtp,rtcp,rtsp"; + + // Default FFmpeg verbosity. Warning (not Info) keeps native->managed call + // volume low, per the too-many-threads note below; adjust at runtime via + // SetLogLevel. + private const UUAVLogLevel DefaultLogLevel = UUAVLogLevel.Warning; + + // TODO make sure to don't overabuse the calls from native parts using too many threads (we had a similar issue before) + // rooted for the process lifetime: the native side keeps these fn pointers + private static readonly ErrorCallback errorCallback = OnNativeError; + private static readonly LogCallback warningCallback = OnNativeWarning; + private static readonly LogCallback logCallback = OnNativeLog; + + private static IntPtr renderCallback; + + public static bool Initialized => NativeMethods.uuav_status().Initialized; + + // pass to GL.IssuePluginEvent with the player id as the event id + public static IntPtr RenderCallback + { + get + { + if (renderCallback == IntPtr.Zero) + { + renderCallback = NativeMethods.uuav_get_render_callback(); + } + + return renderCallback; + } + } + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + private static void Init() + { + if (Initialized) + { + return; + } + +#if UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX + // native reinterprets the probe texture pointer as an + // id with no way to validate it (no QueryInterface + // analog); refusing to init on any other device is the only gate + if (SystemInfo.graphicsDeviceType != UnityEngine.Rendering.GraphicsDeviceType.Metal) + { + Debug.LogError( + $"[UUAV] init: unsupported graphics API {SystemInfo.graphicsDeviceType}; macOS requires Metal" + ); + return; + } +#endif + + Application.quitting += Deinit; + + var config = AudioSettings.GetConfiguration(); + var audioOptions = AudioOptions.FromConfig(config); + + var protocols = UNTRUSTED_STREAMING_PROTOCOLS; +#if UNITY_EDITOR + // Local file playback is a convenience for editing/authoring only; + // it stays out of player builds where URLs are untrusted. + protocols += ",file"; +#endif + + using var probe = ProbeTexture.New(); + var result = NativeMethods.uuav_init(probe.NativePtr(), audioOptions, errorCallback, warningCallback, logCallback, protocols, (int)DefaultLogLevel); + + if (result.IsOk == false) + { + Debug.LogError($"[UUAV] init: {result.ConsumeError()}"); + } + + + // TODO react to AudioSettings.OnAudioConfigurationChanged: + // uuav_update_audio_out and players rebuild their audio path + } + + // Idempotent teardown. Resets the cached render callback so a + // subsequent Init in a fresh domain re-fetches it from the DLL. + private static void Deinit() + { + if (Initialized == false) + { + return; + } + + NativeMethods.uuav_deinit(); + renderCallback = IntPtr.Zero; + } + +#if UNITY_EDITOR + [UnityEditor.InitializeOnLoadMethod] + private static void RegisterDomainReloadTeardown() + { + UnityEditor.AssemblyReloadEvents.beforeAssemblyReload -= Deinit; + UnityEditor.AssemblyReloadEvents.beforeAssemblyReload += Deinit; + } +#endif + + + // called from native playback threads; reads only - native owns the string + // Logging is thread-safe + [MonoPInvokeCallback(typeof(ErrorCallback))] + private static void OnNativeError(IntPtr message) + { + Debug.LogError($"[UUAV] {Utf8.PtrToString(message)}"); + } + + // FFmpeg diagnostics, routed by native by severity. Same threading + // contract as OnNativeError: any FFmpeg thread, native owns the string. + [MonoPInvokeCallback(typeof(LogCallback))] + private static void OnNativeWarning(IntPtr message) + { + Debug.LogWarning($"[UUAV] {Utf8.PtrToString(message)}"); + } + + [MonoPInvokeCallback(typeof(LogCallback))] + private static void OnNativeLog(IntPtr message) + { + Debug.Log($"[UUAV] {Utf8.PtrToString(message)}"); + } + + // Adjust FFmpeg verbosity at runtime (no-op if not yet initialized). + public static void SetLogLevel(UUAVLogLevel level) + { + NativeMethods.uuav_set_log_level((int)level); + } + +#if UNITY_EDITOR + [UnityEditor.MenuItem("UUAV/Init")] + public static void ManualInit() + { + Init(); + } + + [UnityEditor.MenuItem("UUAV/Deinit")] + public static void ManualDeinit() + { + Deinit(); + } + + [UnityEditor.MenuItem("UUAV/Status")] + public static void PrintStats() + { + Status status = NativeMethods.uuav_status(); + + StringBuilder sb = new StringBuilder(); + sb.AppendLine("[UUAV] Status:"); + sb.Append("Initialized: ").AppendLine(status.Initialized.ToString()); + sb.Append("Players Count: ").AppendLine(status.PlayersCount.ToString()); + sb.Append("Audio Channels: ").AppendLine(status.AudioOptions.Channels.ToString()); + sb.Append("Audio SampleRate: ").AppendLine(status.AudioOptions.SampleRate.ToString()); + sb.Append("Device Reason: ").AppendLine(status.ConsumeDeviceRemoveReason()); + sb.AppendLine("[UUAV] :Status"); + + Debug.Log(sb.ToString()); + } +#endif + + private readonly struct ProbeTexture : IDisposable + { + private readonly Texture2D probe; + + private ProbeTexture(Texture2D probe) + { + this.probe = probe; + } + + public static ProbeTexture New() + { + var probe = new Texture2D(1, 1, TextureFormat.RGBA32, mipChain: false); + return new ProbeTexture(probe); + } + + public void Dispose() + { + UnityEngine.Object.DestroyImmediate(probe); + } + + public IntPtr NativePtr() + { + return probe.GetNativeTexturePtr(); + } + } + } +} diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/UUAVRuntime.cs.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/UUAVRuntime.cs.meta new file mode 100644 index 00000000000..be748933c5e --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/UUAVRuntime.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: dbe139ae5c7f0554cae0e516fb2997ca \ No newline at end of file diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/csc.rsp b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/csc.rsp new file mode 100644 index 00000000000..dcc377f8979 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/csc.rsp @@ -0,0 +1 @@ +-nullable:enable \ No newline at end of file diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/csc.rsp.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/csc.rsp.meta new file mode 100644 index 00000000000..3f1f7fa1f1f --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/Runtime/csc.rsp.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 8b95f7d29f7cf45469b73fe68a608fca +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/package.json b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/package.json new file mode 100644 index 00000000000..58e1efcab68 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/package.json @@ -0,0 +1,19 @@ +{ + "name": "com.nickkhalow.uuav", + "version": "0.0.1", + "displayName": "UUAV", + "description": "FFmpeg-based video/audio player with a native Rust core and Unity integration", + "unity": "2019.1", + "keywords": [ + "video", + "audio", + "ffmpeg", + "media", + "player" + ], + "author": { + "name": "Nick Khalow", + "url": "https://github.com/NickKhalow", + "email": "nickhalow@gmail.com" + } +} diff --git a/Explorer/Assets/Plugins/UUAV/Packages/UUAV/package.json.meta b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/package.json.meta new file mode 100644 index 00000000000..d059ab9b137 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/UUAV/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 0d47b679ee3d87a418d51b96dc590ac8 +PackageManifestImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/Packages/manifest.json b/Explorer/Assets/Plugins/UUAV/Packages/manifest.json new file mode 100644 index 00000000000..18a1e415e56 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/manifest.json @@ -0,0 +1,3 @@ +{ + "dependencies": {} +} diff --git a/Explorer/Assets/Plugins/UUAV/Packages/manifest.json.meta b/Explorer/Assets/Plugins/UUAV/Packages/manifest.json.meta new file mode 100644 index 00000000000..2dbfb816070 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/Packages/manifest.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d2729edc526e848b399cab18a4b9a5d4 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/README.md b/Explorer/Assets/Plugins/UUAV/README.md new file mode 100644 index 00000000000..c9cb5736da7 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/README.md @@ -0,0 +1,128 @@ +# UUAV (Ultimately United Audio/Video) + +A video/audio player for Unity built on a native Rust core and FFmpeg, with hardware-accelerated video decoding: D3D11VA on Windows, VideoToolbox on macOS (Apple Silicon + Metal). + +- `native/` - Rust crate compiled to `uuav.dll` / `libuuav.dylib` (`cdylib`, C ABI) +- `Packages/UUAV/` - Unity package (`com.nickkhalow.uuav`) with the C# bindings and the `UUAVPlayer` component + +## How it works + +The C# side is a thin binding layer. `UUAVRuntime` initializes the native runtime once at startup: it passes a probe texture (that's how the plugin captures Unity's graphics device - D3D11 or Metal), the audio output format and an error callback. Everything else goes through `UUAVPlayer`, a MonoBehaviour that talks to the native player by id. + +On the native side each player owns a dedicated playback thread that demuxes and decodes the media. Video is decoded on the GPU (D3D11VA on Windows, VideoToolbox on macOS; hardware decode is mandatory on both, NV12 only). When a frame is due, Unity's render thread (via `GL.IssuePluginEvent`) triggers a copy of the decoded surface into presentation textures: on Windows one shared NV12 texture whose Y/UV planes C# wraps as two format-cast views, on macOS two per-plane `MTLTexture`s (Y `R8Unorm`, UV `RG8Unorm`) blitted from the decoded frame's IOSurface. C# wraps them with `Texture2D.CreateExternalTexture` and blits to a regular `RenderTexture` through the `Hidden/UUAV/NV12ToRGB` shader. That `RenderTexture` is what you get from `player.CurrentTexture`. + +Audio is decoded and resampled to interleaved `f32` at the engine's output format, buffered in a lock-free ring, and pulled by Unity from `OnAudioFilterRead`. Playback is paced by a master media clock, which can also be slaved to an external clock via `AssignMasterClock` (useful for syncing multiple players). + +Opening a URL is async and cancellable; closing never blocks the engine - playback threads are cancelled and abandoned, never joined on the hot path. + +### Basic usage + +```csharp +var player = UUAVPlayer.New(); // or add the component in the editor +player.OpenMedia("https://example.com/video.mp4"); +player.Play(); + +// render player.CurrentTexture (RenderTexture) anywhere - UI RawImage, material, etc. +``` + +See `Packages/UUAV/Example/UIExamplePlayer.cs` for a working example. + +## Why Rust + +Mostly safety: this is code that shares a D3D11 device and threads with the engine, and memory/thread-safety bugs there are miserable to debug. Rust catches most of them at compile time; the unsafe surface is confined to the FFI boundary, FFmpeg calls and D3D interop. + +The other reason is reusability. The output is a plain C-ABI DLL with nothing engine-specific baked in - the same `uuav.dll` can be embedded in other engines or apps (Unreal, Godot, whatever), only the thin C# binding layer is Unity-specific. + +## Building + +### Windows + +Prerequisites: + +- Rust toolchain with the GNU target: `rustup target add x86_64-pc-windows-gnu` +- MSYS2 with the mingw64 toolchain at `C:\msys64` (the linker is pinned to `C:\msys64\mingw64\bin\gcc.exe` in `native/.cargo/config.toml`) +- FFmpeg **8.1** development files (headers + import libs) in `native/.third_party/ffmpeg/`. Use the [BtbN](https://github.com/BtbN/FFmpeg-Builds/releases) **LGPL shared** win64 build. `FFMPEG_DIR` already points there via `native/.cargo/config.toml`. + +Then: + +```bash +cd native +./build.sh +``` + +This runs `cargo build --release --target x86_64-pc-windows-gnu` and copies `uuav.dll` into `Packages/UUAV/Runtime/Plugins/x86_64/`. + +### macOS (Apple Silicon only) + +Prerequisites: + +- Xcode (or the Command Line Tools) for clang, Metal frameworks and `codesign` +- Rust toolchain with the native target: `rustup target add aarch64-apple-darwin` + +FFmpeg is built from source once (the macOS analog of dropping the BtbN build into `.third_party/ffmpeg`): + +```bash +cd native +./scripts/build-ffmpeg-macos.sh # clones FFmpeg n8.1, LGPL shared build into .third_party/ffmpeg +./build.sh +``` + +`build.sh` compiles `libuuav.dylib`, copies it plus the seven FFmpeg dylibs into `Packages/UUAV/Runtime/Plugins/macOS/` and ad-hoc code-signs everything (mandatory on arm64). + +Dylib loading needs no `install_name_tool` post-processing: FFmpeg is configured with `--install-name-dir='@rpath'` and `libuuav.dylib` carries an `LC_RPATH @loader_path` entry (set in `native/.cargo/config.toml`), so the whole set resolves from the plugin folder - both in the editor and in a built player (`Contents/PlugIns/`). Verify with `otool -L libuuav.dylib`. + +## Runtime deployment + +`uuav.dll` dynamically links FFmpeg, so the FFmpeg runtime DLLs must sit next to `uuav.dll` (`Packages/UUAV/Runtime/Plugins/x86_64/` in-project, or the plugins folder of a built player). The exact set, taken from the FFmpeg **8.1** BtbN LGPL shared build: + +``` +avcodec-63.dll +avdevice-63.dll +avfilter-12.dll +avformat-63.dll +avutil-61.dll +libbz2-1.dll +libiconv-2.dll +liblzma-5.dll +libwinpthread-1.dll +swresample-7.dll +swscale-10.dll +zlib1.dll +``` + +The library major versions (avcodec **63**, avformat **63**, avutil **61**, swresample **7**, swscale **10**, avfilter **12**, avdevice **63**) are tied to the FFmpeg 8.1 release. A different FFmpeg version ships differently-named DLLs (`avcodec-62.dll`, …) that `uuav.dll` won't load - keep the DLL set and `ffmpeg-sys-next = "8.1.0"` in `native/Cargo.toml` in lockstep. + +A few notes: + +- `libwinpthread-1.dll` is mandatory. The FFmpeg chain links against it; if it isn't next to the plugin, the loader falls back to `PATH` (which may hold an incompatible copy, e.g. Git's older mingw64) and `LoadLibrary` fails with `ERROR_PROC_NOT_FOUND (127)`. +- FFmpeg is used under LGPL as a dynamically-linked shared library; see the license shipped with the BtbN build. + +On macOS the equivalent set lives in `Packages/UUAV/Runtime/Plugins/macOS/` (deployed by `build.sh`; the majors are from the FFmpeg n8.1 source build and differ from the BtbN win64 numbering): + +``` +libuuav.dylib +libavcodec.62.dylib +libavdevice.62.dylib +libavfilter.11.dylib +libavformat.62.dylib +libavutil.60.dylib +libswresample.6.dylib +libswscale.9.dylib +``` + +No extra support libraries are needed: https uses SecureTransport (an OS framework), and zlib/bzip2/iconv come from the OS. + +macOS notes: + +- **Apple Silicon only** (`aarch64-apple-darwin`), **Metal only**: `UUAVRuntime` refuses to initialize on any other graphics API, because native has no way to validate that the probe texture pointer is an `id`. +- The Unity editor must be the **arm64-native** build - a Rosetta (Intel) editor cannot load an arm64 dylib. + +## GPU device synchronization + +The native decoder shares Unity's D3D11 device (captured from a probe texture at init, see `native/src/hw_device.rs`). The `ID3D11Device` itself is free-threaded, but the immediate context is not - D3D11VA decode calls on the playback thread would otherwise race Unity's render thread and hang the runtime. + +`SetMultithreadProtected(true)` used for the immediate context, performance affection is negligible in practice. All it does is wrap each context call in an internal critical section: uncontended that's nanoseconds, and contention only happens during the short window where a decoded frame is copied into the presentation texture. Next to the actual GPU work and Unity's own command submission this is noise based on the investigation. + +The alternative - a separate D3D11 device for FFmpeg with frames passed across via `D3D11_RESOURCE_MISC_SHARED` textures - was considered and rejected. It removes immediate-context contention entirely, but adds cross-device texture lifetime management, per-frame open/acquire/release complexity and additional failure surface, all to avoid a cost that was already negligible. + +None of this applies on macOS: VideoToolbox owns its decode pipeline end to end, the plugin blits on its own `MTLCommandQueue`, and `MTLDevice` is free-threaded. The per-frame blit is committed with `waitUntilCompleted`, which both orders it against Unity's queue and guarantees the borrowed decoded frame outlives the GPU reads; replacing that wait with `MTLSharedEvent`/`IUnityGraphicsMetal` synchronization is deliberately deferred optimization headroom. diff --git a/Explorer/Assets/Plugins/UUAV/README.md.meta b/Explorer/Assets/Plugins/UUAV/README.md.meta new file mode 100644 index 00000000000..855118aacb9 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 970b19feb680c3d4bbac9dd30ce8467c +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/native.meta b/Explorer/Assets/Plugins/UUAV/native.meta new file mode 100644 index 00000000000..ac428313cc4 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fdecc6c2d39bada4695ba62867e0da64 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/Plugins/UUAV/native/.cargo/config.toml b/Explorer/Assets/Plugins/UUAV/native/.cargo/config.toml new file mode 100644 index 00000000000..a3eae19e1ef --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/.cargo/config.toml @@ -0,0 +1,16 @@ +[env] +# LGPL shared build of FFmpeg 8.1 (BtbN); see .third_party/ffmpeg/LICENSE.txt +FFMPEG_DIR = { value = ".third_party/ffmpeg", relative = true } + +[build] +target-dir = ".target" + + +# TODO why exactly this linker? +[target.x86_64-pc-windows-gnu] +linker = "C:\\msys64\\mingw64\\bin\\gcc.exe" + +# @loader_path rpath: libuuav.dylib resolves the @rpath/... FFmpeg dylibs +# from its own folder, in-editor and in a built player alike +[target.aarch64-apple-darwin] +rustflags = ["-C", "link-arg=-Wl,-rpath,@loader_path"] diff --git a/Explorer/Assets/Plugins/UUAV/native/.gitignore b/Explorer/Assets/Plugins/UUAV/native/.gitignore new file mode 100644 index 00000000000..8b4b2c32d6b --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/.gitignore @@ -0,0 +1,31 @@ +# Generated by Cargo +# will have compiled files and executables +debug +target +.target + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# Generated by cargo mutants +# Contains mutation testing data +**/mutants.out*/ + +# rustc will dump stack traces when hitting an internal compiler error to PWD +rustc-ice-*.txt + +# RustRover +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# machine-built FFmpeg (see scripts/build-ffmpeg-macos.sh / README); not committed +.third_party +.ffmpeg-src + +**/*.meta diff --git a/Explorer/Assets/Plugins/UUAV/native/Cargo.lock b/Explorer/Assets/Plugins/UUAV/native/Cargo.lock new file mode 100644 index 00000000000..3c87fba1a28 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/Cargo.lock @@ -0,0 +1,711 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "cc" +version = "1.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +dependencies = [ + "find-msvc-tools", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "objc2", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "ffmpeg-sys-next" +version = "8.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a314bc0e022a33a99567ed4bd2576bd58ffd8fcff7891c29194cfecc26a62547" +dependencies = [ + "bindgen", + "cc", + "libc", + "num_cpus", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", + "objc2-metal", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags", + "objc2", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags", + "libc", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags", + "block2", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-io-surface", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "ringbuf" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a158e09ede21a14b172ca6cdd6208386c6ae2cb6acef58d774368ef8c450dfa7" +dependencies = [ + "crossbeam-utils", + "portable-atomic", + "portable-atomic-util", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "uuav" +version = "0.2.0" +dependencies = [ + "anyhow", + "arc-swap", + "crossbeam-channel", + "dashmap", + "ffmpeg-sys-next", + "libc", + "objc2", + "objc2-core-video", + "objc2-io-surface", + "objc2-metal", + "parking_lot", + "ringbuf", + "windows", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Explorer/Assets/Plugins/UUAV/native/Cargo.toml b/Explorer/Assets/Plugins/UUAV/native/Cargo.toml new file mode 100644 index 00000000000..73eda452a79 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "uuav" +version = "0.2.0" +edition = "2024" + +[dependencies] +anyhow = "1.0.98" +arc-swap = "1.7.1" +crossbeam-channel = "0.5.16" +dashmap = "6.1.0" + +# NOTE: ffmpeg-next 8.1.0 does not compile against the bundled FFmpeg 8.1 +# headers (deprecated AVCodec fields removed, new side-data enum variants), +# so the player is built directly on the sys bindings. +ffmpeg-sys-next = "8.1.0" + +libc = "0.2" +parking_lot = "0.12" +ringbuf = "0.5" + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.61", features = [ + "Win32_Foundation", + "Win32_Graphics_Direct3D", + "Win32_Graphics_Direct3D11", + "Win32_Graphics_Dxgi_Common", +] } + +# objc2 over the old gfx-rs `metal` crate: actively maintained, and its +# Retained RAII with Option-returning constructors fits the crate's +# no-panic rule (the metal crate panics internally). +[target.'cfg(target_os = "macos")'.dependencies] +objc2 = "0.6" +# the objc2-io-surface feature unlocks the IOSurface bridging APIs +# (newTextureWithDescriptor:iosurface:plane:, CVPixelBufferGetIOSurface) +objc2-metal = { version = "0.3", features = ["objc2-io-surface"] } +objc2-core-video = { version = "0.3", features = ["objc2-io-surface"] } +objc2-io-surface = "0.3" + +[lib] +name = "uuav" +crate-type = ["cdylib"] diff --git a/Explorer/Assets/Plugins/UUAV/native/build.sh b/Explorer/Assets/Plugins/UUAV/native/build.sh new file mode 100755 index 00000000000..d78fed68e2a --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/build.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +set -e # Stop on error + +case "$(uname -s)" in +Darwin) + TARGET="aarch64-apple-darwin" # rpath flags configured in .cargo/config.toml + DEST_DIR="../Packages/UUAV/Runtime/Plugins/macOS" + FFMPEG_LIB=".third_party/ffmpeg/lib" + + if [[ ! -d "$FFMPEG_LIB" ]]; then + echo "error: $FFMPEG_LIB is missing; run scripts/build-ffmpeg-macos.sh first" >&2 + exit 1 + fi + + cargo build --release --target "$TARGET" + + mkdir -p "$DEST_DIR" + cp ".target/$TARGET/release/libuuav.dylib" "$DEST_DIR/" + + # deploy each FFmpeg dylib under its major-version name (dereferencing + # the libavutil.60.dylib -> libavutil.60.26.100.dylib symlink): that is + # the exact name the @rpath install-name references resolve to + for lib in avcodec avdevice avfilter avformat avutil swresample swscale; do + major=$(find "$FFMPEG_LIB" -type l -name "lib$lib.*.dylib" | grep -E "lib$lib\.[0-9]+\.dylib$") + cp -L "$major" "$DEST_DIR/" + done + + # ad-hoc code signing is mandatory on arm64; the copies above invalidate + # whatever signature the build produced + codesign -f -s - "$DEST_DIR"/*.dylib + + echo "Deployed to: $DEST_DIR" + ;; +*) + TARGET="x86_64-pc-windows-gnu" # linker configured in .cargo/config.toml + DEST_DIR="../Packages/UUAV/Runtime/Plugins/x86_64" + + cargo build --release --target "$TARGET" + + mkdir -p "$DEST_DIR" + cp ".target/$TARGET/release/uuav.dll" "$DEST_DIR/" + + echo "Deployed to: $DEST_DIR" + echo "Make sure to provied libwinpthread-1.dll and ffmpeg binaries mentioned in the readme file" + ;; +esac diff --git a/Explorer/Assets/Plugins/UUAV/native/scripts/build-ffmpeg-macos.sh b/Explorer/Assets/Plugins/UUAV/native/scripts/build-ffmpeg-macos.sh new file mode 100755 index 00000000000..2f234aa88eb --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/scripts/build-ffmpeg-macos.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# Builds FFmpeg 8.1 (LGPL, shared) from source for Apple Silicon into +# native/.third_party/ffmpeg - the macOS analog of the BtbN win64 build the +# Windows side consumes from the same path. +# +# --install-name-dir='@rpath' stamps every dylib's install name (and the +# inter-library references) as @rpath/lib*.dylib, so libuuav.dylib resolves +# the whole set from its own folder via its LC_RPATH @loader_path entry - +# no install_name_tool post-processing, works in-editor and in built players. + +set -euo pipefail + +# FFmpeg release in lockstep with ffmpeg-sys-next in Cargo.toml +FFMPEG_TAG="n8.1" + +NATIVE_DIR="$(cd "$(dirname "$0")/.." && pwd)" +SRC_DIR="$NATIVE_DIR/.ffmpeg-src" +PREFIX="$NATIVE_DIR/.third_party/ffmpeg" + +if [[ "$(uname -s)" != "Darwin" || "$(uname -m)" != "arm64" ]]; then + echo "error: this script targets Apple Silicon macOS only" >&2 + exit 1 +fi + +export MACOSX_DEPLOYMENT_TARGET=11.0 + +if [[ ! -d "$SRC_DIR" ]]; then + git clone --depth 1 --branch "$FFMPEG_TAG" https://github.com/FFmpeg/FFmpeg.git "$SRC_DIR" +fi + +cd "$SRC_DIR" + +# securetransport is the schannel analog: https support with zero extra +# dylibs. If a future FFmpeg drops it, switch to --enable-openssl (adds a +# homebrew openssl runtime dependency). +if ! ./configure --help | grep -q securetransport; then + echo "error: this FFmpeg has no --enable-securetransport; use --enable-openssl instead" >&2 + exit 1 +fi + +# LGPL is the default (no --enable-gpl). Do NOT disable avdevice/avfilter: +# ffmpeg-sys-next's default features link all seven libraries. +./configure \ + --prefix="$PREFIX" \ + --install-name-dir='@rpath' \ + --arch=arm64 \ + --enable-shared \ + --disable-static \ + --disable-programs \ + --disable-doc \ + --enable-videotoolbox \ + --enable-securetransport + +make -j"$(sysctl -n hw.ncpu)" +make install + +echo +echo "Installed into: $PREFIX" +echo "Install names (all must be @rpath/...):" +for lib in "$PREFIX"/lib/lib*.dylib; do + [[ -L "$lib" ]] && continue + otool -D "$lib" | tail -1 +done diff --git a/Explorer/Assets/Plugins/UUAV/native/src/audio_decoder.rs b/Explorer/Assets/Plugins/UUAV/native/src/audio_decoder.rs new file mode 100644 index 00000000000..7aba4f1a84a --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/src/audio_decoder.rs @@ -0,0 +1,274 @@ +use anyhow::{Context, Result, ensure}; +use ffmpeg_sys_next as ff; +use std::mem; +use std::os::raw::c_int; +use std::ptr; + +use crate::AudioOptions; +use crate::ffutil::{ + AVERROR_EAGAIN, Decoded, OwnedChannelLayout, OwnedDecoder, OwnedFrame, OwnedSwr, Stream, + av_err, check, q2d, +}; + +/// Interleaved f32 samples in the configured output format. +pub(crate) struct AudioFrame { + pts: Option, + samples: Vec, +} + +impl AudioFrame { + /// Presentation time in seconds of the first sample, `None` when the + /// container provides no usable timestamp. + pub(crate) const fn pts(&self) -> Option { + self.pts + } + + pub(crate) fn samples(&self) -> &[f32] { + &self.samples + } +} + +/// Description of the samples entering the resampler; +/// a change (rare, but legal mid-stream) triggers a rebuild. +struct SwrInput { + format: c_int, + rate: c_int, + layout: OwnedChannelLayout, +} + +impl SwrInput { + fn from_frame_options(frame: &OwnedFrame) -> Result { + Ok(Self { + format: frame.format(), + rate: frame.sample_rate(), + layout: OwnedChannelLayout::copied_from(frame.ch_layout())?, + }) + } +} + +/// Resampler converting decoded frames to interleaved `f32` at the output +/// rate and channel count it was built for +struct Resampler { + swr: OwnedSwr, + input: SwrInput, + output: AudioOptions, + /// The input rate the resampler was configured with: the stream's real + /// rate scaled by the playback rate (varispeed), so one media second + /// converts into `1 / rate` engine seconds and pitch follows. + scaled_in_rate: c_int, +} + +impl Resampler { + /// Builds a resampler converting from the frame's sample format to + /// interleaved `f32` at `output`, sped up by `playback_rate`. + fn new(frame: &OwnedFrame, output: AudioOptions, playback_rate: f64) -> Result { + let input = SwrInput::from_frame_options(frame)?; + let scaled = (f64::from(input.rate) * playback_rate).round(); + ensure!( + scaled >= 1.0 && scaled <= f64::from(c_int::MAX), + "playback rate {playback_rate} is out of range for a {} Hz stream", + input.rate + ); + let scaled_in_rate = scaled as c_int; + let out_layout = OwnedChannelLayout::default_for(output.channels); + let swr = OwnedSwr::new( + out_layout.as_ref(), + ff::AVSampleFormat::AV_SAMPLE_FMT_FLT, + output.sample_rate, + frame.ch_layout(), + unsafe { mem::transmute::(input.format) }, + scaled_in_rate, + )?; + + Ok(Self { + swr, + input, + output, + scaled_in_rate, + }) + } + + /// Whether the frame still matches the input this resampler was built + /// for. + fn matches(&self, frame: &OwnedFrame) -> bool { + self.input.format == frame.format() + && self.input.rate == frame.sample_rate() + && self.input.layout.matches(frame.ch_layout()) + } + + /// Converts the frame's samples into interleaved `f32` at the output + /// rate. + fn convert(&mut self, frame: &OwnedFrame) -> Result> { + let in_rate = frame.sample_rate(); + ensure!(in_rate > 0, "invalid audio frame sample rate: {in_rate}"); + let in_samples = frame.nb_samples(); + // the resampler counts input samples in its own (rate-scaled) domain + let delay = self + .swr + .apply_delay_and_modify(i64::from(self.scaled_in_rate)); + + let out_capacity: c_int = { + let pending_in_samples = delay as f64 + f64::from(in_samples); + let rate_ratio = self.output.sample_rate_f64() / f64::from(self.scaled_in_rate); + let exact_out_samples = pending_in_samples * rate_ratio; + // generous headroom over the exact rescale to absorb rounding + (exact_out_samples.ceil() + 32.0) as c_int + }; + + let channels = self.output.channels_usize(); + let out_capacity_usize = + usize::try_from(out_capacity).context("negative resampler output capacity")?; + let out_len = out_capacity_usize + .checked_mul(channels.get()) + .context("resampler output size overflows usize")?; + + let mut samples = vec![0.0_f32; out_len]; + + let out_planes = [samples.as_mut_ptr().cast::()]; + + // SAFETY: `samples` has room for `out_capacity` interleaved output + // samples; the frame's planes hold `in_samples` input samples + let converted = unsafe { + self.swr.convert( + out_planes.as_ptr(), + out_capacity, + frame.extended_data(), + in_samples, + ) + }?; + + let converted_len = usize::try_from(converted) + .context("negative converted sample count")? + .checked_mul(channels.get()) + .context("converted sample count overflows usize")?; + samples.truncate(converted_len); + Ok(samples) + } +} + +/// Audio decoder + resampler producing interleaved `f32` at the sample rate +/// and channel count of the engine's audio output. +pub(crate) struct AudioDecoder { + ctx: OwnedDecoder, + time_base: ff::AVRational, + resampler: Option, + audio_options: AudioOptions, + /// Varispeed factor baked into the resampler ratio; see + /// [`Resampler::scaled_in_rate`]. + playback_rate: f64, +} + +impl AudioDecoder { + pub(crate) fn new(stream: Stream, audio_options: AudioOptions) -> Result { + let codec = stream.find_decoder().context("audio stream")?; + let mut ctx = OwnedDecoder::new(codec)?; + + unsafe { + check( + "avcodec_parameters_to_context(audio)", + ff::avcodec_parameters_to_context(ctx.as_mut_ptr(), stream.codecpar()), + )?; + (*ctx.as_mut_ptr()).pkt_timebase = stream.time_base(); + check( + "avcodec_open2(audio)", + ff::avcodec_open2(ctx.as_mut_ptr(), codec, ptr::null_mut()), + )?; + } + + Ok(Self { + ctx, + time_base: stream.time_base(), + resampler: None, + audio_options, + playback_rate: 1.0, + }) + } + + /// Applies new output options. Returns `true` when the format changed + /// (callers should discard already-buffered samples). + pub(crate) fn set_output(&mut self, options: AudioOptions) -> bool { + if options == self.audio_options { + return false; + } + self.audio_options = options; + self.resampler = None; + true + } + + /// Applies a new playback rate. Returns `true` when it changed + /// (callers should discard already-buffered samples). + pub(crate) fn set_rate(&mut self, rate: f64) -> bool { + if rate.to_bits() == self.playback_rate.to_bits() { + return false; + } + self.playback_rate = rate; + self.resampler = None; + true + } + + pub(crate) const fn audio_options(&self) -> AudioOptions { + self.audio_options + } + + /// Sends a packet; pass `null` to signal end of stream. + pub(crate) fn send(&mut self, packet: *const ff::AVPacket) -> Result<()> { + let ret = unsafe { ff::avcodec_send_packet(self.ctx.as_mut_ptr(), packet) }; + if ret < 0 && ret != ff::AVERROR_EOF { + return Err(av_err("avcodec_send_packet(audio)", ret)); + } + Ok(()) + } + + pub(crate) fn receive(&mut self) -> Result> { + let mut frame = OwnedFrame::new()?; + let ret = unsafe { ff::avcodec_receive_frame(self.ctx.as_mut_ptr(), frame.as_mut_ptr()) }; + if ret == AVERROR_EAGAIN { + return Ok(Decoded::Again); + } + if ret == ff::AVERROR_EOF { + return Ok(Decoded::Eof); + } + check("avcodec_receive_frame(audio)", ret)?; + + let resampler = self.relevant_resampler_for_frame_or_new(&frame)?; + let samples = resampler.convert(&frame)?; + + let ts = frame.best_effort_timestamp(); + let pts = self.pts_from_timestamp(ts); + + Ok(Decoded::Frame(AudioFrame { pts, samples })) + } + + const fn pts_from_timestamp(&self, timestamp: i64) -> Option { + if timestamp == ff::AV_NOPTS_VALUE { + None + } else { + Some(timestamp as f64 * q2d(self.time_base)) + } + } + + fn relevant_resampler_for_frame_or_new( + &mut self, + frame: &OwnedFrame, + ) -> Result<&mut Resampler> { + let relevant = match self.resampler.take() { + Some(resampler) => { + if resampler.matches(frame) { + resampler + } else { + Resampler::new(frame, self.audio_options, self.playback_rate)? + } + } + None => Resampler::new(frame, self.audio_options, self.playback_rate)?, + }; + + let relevant = self.resampler.insert(relevant); + Ok(relevant) + } + + pub(crate) fn flush(&mut self) { + unsafe { ff::avcodec_flush_buffers(self.ctx.as_mut_ptr()) }; + // discard resampler history along with the decoder state + self.resampler = None; + } +} diff --git a/Explorer/Assets/Plugins/UUAV/native/src/ffutil.rs b/Explorer/Assets/Plugins/UUAV/native/src/ffutil.rs new file mode 100644 index 00000000000..ecad99c755d --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/src/ffutil.rs @@ -0,0 +1,400 @@ +//! Thin RAII wrappers and error helpers over `ffmpeg-sys-next`. + +use anyhow::{Result, anyhow, ensure}; +use ffmpeg_sys_next as ff; +use std::ffi::{CStr, CString}; +use std::num::NonZeroI32; +use std::os::raw::{c_char, c_int}; + +/// "Decoder needs more input" return code. FFmpeg has no named define for +/// it: the C API spells it `AVERROR(EAGAIN)`, with `EAGAIN` coming from the +/// platform's errno, so the bindings can only offer the same composition. +pub(crate) const AVERROR_EAGAIN: c_int = ff::AVERROR(libc::EAGAIN); + +/// Outcome of a receive call on a decoder. +pub(crate) enum Decoded { + Frame(T), + /// Decoder needs more input. + Again, + /// Decoder is fully drained. + Eof, +} + +/// Formats an FFmpeg error code into an [`anyhow::Error`] with context. +pub(crate) fn av_err(context: &str, code: c_int) -> anyhow::Error { + let mut buf = [0 as c_char; 64]; + let message = unsafe { + if ff::av_strerror(code, buf.as_mut_ptr(), buf.len()) == 0 { + CStr::from_ptr(buf.as_ptr()).to_string_lossy().into_owned() + } else { + format!("ffmpeg error {code}") + } + }; + anyhow!("{context}: {message}") +} + +/// Returns the code unchanged for non-negative FFmpeg return codes. +pub(crate) fn check(context: &str, code: c_int) -> Result { + if code < 0 { + Err(av_err(context, code)) + } else { + Ok(code) + } +} + +pub(crate) const fn q2d(r: ff::AVRational) -> f64 { + if r.den == 0 { + 0.0 + } else { + r.num as f64 / r.den as f64 + } +} + +/// Copies a NUL-terminated C string into a fixed name buffer, truncating +/// to leave room for the terminator; a null `src` yields an empty string. +pub(crate) fn copy_c_name(dst: &mut [c_char; N], src: *const c_char) { + let bytes: &[u8] = if src.is_null() { + &[] + } else { + unsafe { CStr::from_ptr(src) }.to_bytes() + }; + + let len = bytes.len().min(N.saturating_sub(1)); + for (d, &s) in dst.iter_mut().zip(bytes.iter().take(len)) { + *d = c_char::from_ne_bytes([s]); + } + + // terminate right after the copied bytes: dst may hold a longer + // previous name, and position `len` always exists for N > 0 + if let Some(terminator) = dst.get_mut(len) { + *terminator = 0; + } +} + +pub(crate) struct StreamingProtocol(CString); + +impl StreamingProtocol { + /// Safety + /// `raw` must be null or point to a NUL-terminated C string. + pub(crate) unsafe fn new(raw: *const c_char) -> Result { + ensure!(!raw.is_null(), "protocol_whitelist is null"); + let value = unsafe { CStr::from_ptr(raw) }; + ensure!(!value.to_bytes().is_empty(), "protocol_whitelist is empty"); + Ok(Self(value.to_owned())) + } +} + +/// Owning wrapper around an `AVDictionary`. +pub(crate) struct AvDict(*mut ff::AVDictionary); + +impl AvDict { + #[allow(clippy::needless_pass_by_ref_mut)] // hands out a mutable alias + pub(crate) const fn as_mut_ptr(&mut self) -> *mut *mut ff::AVDictionary { + &raw mut self.0 + } +} + +impl From<&StreamingProtocol> for AvDict { + fn from(protocol: &StreamingProtocol) -> Self { + let mut dict: *mut ff::AVDictionary = std::ptr::null_mut(); + + // Only fails on allocation failure, which leaves `dict` null and + // surfaces later as an open error. + // Copies the protocol value + unsafe { + ff::av_dict_set( + &mut dict, + c"protocol_whitelist".as_ptr(), + protocol.0.as_ptr(), + 0, + ); + } + Self(dict) + } +} + +impl Drop for AvDict { + fn drop(&mut self) { + unsafe { ff::av_dict_free(&mut self.0) }; + } +} + +/// Non-owning view of an `AVStream` belonging to a live format context. +#[derive(Clone, Copy)] +pub(crate) struct Stream(*mut ff::AVStream); + +impl Stream { + /// # Safety + /// `ptr` must point to a stream of a live `AVFormatContext`, and the + /// view must not outlive that context. + pub(crate) const unsafe fn from_raw(ptr: *mut ff::AVStream) -> Self { + Self(ptr) + } + + pub(crate) fn codecpar(self) -> *const ff::AVCodecParameters { + unsafe { (*self.0).codecpar } + } + + pub(crate) fn codec_id(self) -> ff::AVCodecID { + unsafe { (*self.codecpar()).codec_id } + } + + pub(crate) fn time_base(self) -> ff::AVRational { + unsafe { (*self.0).time_base } + } + + pub(crate) fn avg_frame_rate(self) -> ff::AVRational { + unsafe { (*self.0).avg_frame_rate } + } + + pub(crate) fn find_decoder(self) -> Result<*const ff::AVCodec> { + let codec = unsafe { ff::avcodec_find_decoder(self.codec_id()) }; + if codec.is_null() { + return Err(anyhow!("no decoder for codec id {:?}", self.codec_id())); + } + Ok(codec) + } +} + +/// Owning wrapper around an `AVCodecContext`. +pub(crate) struct OwnedDecoder(*mut ff::AVCodecContext); + +impl OwnedDecoder { + pub(crate) fn new(codec: *const ff::AVCodec) -> Result { + let ptr = unsafe { ff::avcodec_alloc_context3(codec) }; + if ptr.is_null() { + return Err(anyhow!("avcodec_alloc_context3 failed")); + } + Ok(Self(ptr)) + } + + #[allow(clippy::needless_pass_by_ref_mut)] // hands out a mutable alias + pub(crate) const fn as_mut_ptr(&mut self) -> *mut ff::AVCodecContext { + self.0 + } +} + +impl Drop for OwnedDecoder { + fn drop(&mut self) { + unsafe { ff::avcodec_free_context(&mut self.0) }; + } +} + +/// Owning wrapper around an `AVFrame`. +/// +/// The frame's data buffers are reference counted by FFmpeg, so moving the +/// wrapper across threads (decoder thread to render thread) is safe. +pub(crate) struct OwnedFrame(*mut ff::AVFrame); + +unsafe impl Send for OwnedFrame {} + +impl OwnedFrame { + pub(crate) fn new() -> Result { + let ptr = unsafe { ff::av_frame_alloc() }; + if ptr.is_null() { + return Err(anyhow!("av_frame_alloc failed")); + } + Ok(Self(ptr)) + } + + #[allow(clippy::needless_pass_by_ref_mut)] // hands out a mutable alias + pub(crate) const fn as_mut_ptr(&mut self) -> *mut ff::AVFrame { + self.0 + } + + /// Pixel format for video frames, sample format for audio frames. + pub(crate) fn format(&self) -> c_int { + unsafe { (*self.0).format } + } + + pub(crate) fn width(&self) -> c_int { + unsafe { (*self.0).width } + } + + pub(crate) fn height(&self) -> c_int { + unsafe { (*self.0).height } + } + + pub(crate) fn best_effort_timestamp(&self) -> i64 { + unsafe { (*self.0).best_effort_timestamp } + } + + pub(crate) fn sample_rate(&self) -> c_int { + unsafe { (*self.0).sample_rate } + } + + pub(crate) fn nb_samples(&self) -> c_int { + unsafe { (*self.0).nb_samples } + } + + /// Channel layout of an audio frame. + pub(crate) fn ch_layout(&self) -> &ff::AVChannelLayout { + unsafe { &(*self.0).ch_layout } + } + + /// Plane pointers of an audio frame, in the shape `swr_convert` takes. + pub(crate) fn extended_data(&self) -> *const *const u8 { + unsafe { (*self.0).extended_data.cast::<*const u8>().cast_const() } + } + + /// Data pointer of one plane; null for planes past + /// `AV_NUM_DATA_POINTERS`. Hardware frames repurpose the planes (for + /// D3D11: 0 is the texture, 1 the array slice index). + pub(crate) fn data(&self, plane: usize) -> *mut u8 { + // copies only the 8-byte plane pointer, never the plane contents + match unsafe { (*self.0).data.get(plane) } { + Some(&pointer) => pointer, + None => std::ptr::null_mut(), + } + } + + /// Hardware frames context of a frame holding hardware surfaces; fails + /// on software frames, which carry none. + pub(crate) fn hw_frames_ctx(&self) -> Result<&ff::AVHWFramesContext> { + unsafe { + let buffer = (*self.0).hw_frames_ctx; + if buffer.is_null() { + return Err(anyhow!("frame has no hw frames context")); + } + Ok(&*(*buffer).data.cast::()) + } + } +} + +impl Drop for OwnedFrame { + fn drop(&mut self) { + unsafe { ff::av_frame_free(&mut self.0) }; + } +} + +/// Owning wrapper around an `AVChannelLayout`. +pub(crate) struct OwnedChannelLayout(ff::AVChannelLayout); + +impl OwnedChannelLayout { + /// Native layout for the given channel count. + pub(crate) fn default_for(channels: NonZeroI32) -> Self { + let mut layout: ff::AVChannelLayout = unsafe { std::mem::zeroed() }; + unsafe { ff::av_channel_layout_default(&mut layout, channels.get()) }; + Self(layout) + } + + /// Deep copy of an existing layout. + pub(crate) fn copied_from(layout: &ff::AVChannelLayout) -> Result { + let mut owned = Self(unsafe { std::mem::zeroed() }); + check("av_channel_layout_copy", unsafe { + ff::av_channel_layout_copy(&mut owned.0, layout) + })?; + Ok(owned) + } + + pub(crate) fn matches(&self, other: &ff::AVChannelLayout) -> bool { + unsafe { ff::av_channel_layout_compare(&self.0, other) == 0 } + } +} + +impl AsRef for OwnedChannelLayout { + fn as_ref(&self) -> &ff::AVChannelLayout { + &self.0 + } +} + +impl Drop for OwnedChannelLayout { + fn drop(&mut self) { + unsafe { ff::av_channel_layout_uninit(&mut self.0) }; + } +} + +/// Owning wrapper around an `SwrContext`. +pub(crate) struct OwnedSwr(*mut ff::SwrContext); + +impl OwnedSwr { + /// Allocates and initializes a resampling context converting between + /// the given formats. + pub(crate) fn new( + out_layout: &ff::AVChannelLayout, + out_format: ff::AVSampleFormat, + out_rate: NonZeroI32, + in_layout: &ff::AVChannelLayout, + in_format: ff::AVSampleFormat, + in_rate: c_int, + ) -> Result { + // wrapped from the first moment, so a failed init still frees + // whatever swr_alloc_set_opts2 allocated + let mut swr = Self(std::ptr::null_mut()); + check("swr_alloc_set_opts2", unsafe { + ff::swr_alloc_set_opts2( + &mut swr.0, + out_layout, + out_format, + out_rate.get(), + in_layout, + in_format, + in_rate, + 0, + std::ptr::null_mut(), + ) + })?; + check("swr_init", unsafe { ff::swr_init(swr.0) })?; + Ok(swr) + } + + /// Upper bound on the samples buffered inside the resampler, in units + /// of `1 / base` (pass the input rate to count input samples). + pub(crate) fn apply_delay_and_modify(&self, base: i64) -> i64 { + unsafe { ff::swr_get_delay(self.0, base) } + } + + /// # Safety + /// `out` must point to writable planes with room for `out_count` + /// samples in the output format; `input` must point to readable planes + /// holding `in_count` samples in the input format. + pub(crate) unsafe fn convert( + &mut self, + out: *const *mut u8, + out_count: c_int, + input: *const *const u8, + in_count: c_int, + ) -> Result { + check("swr_convert", unsafe { + ff::swr_convert(self.0, out, out_count, input, in_count) + }) + } +} + +impl Drop for OwnedSwr { + fn drop(&mut self) { + unsafe { ff::swr_free(&mut self.0) }; + } +} + +/// Owning wrapper around an `AVPacket`. +pub(crate) struct OwnedPacket(*mut ff::AVPacket); + +impl OwnedPacket { + pub(crate) fn new() -> Result { + let ptr = unsafe { ff::av_packet_alloc() }; + if ptr.is_null() { + return Err(anyhow!("av_packet_alloc failed")); + } + Ok(Self(ptr)) + } + + #[allow(clippy::needless_pass_by_ref_mut)] // hands out a mutable alias + pub(crate) const fn as_mut_ptr(&mut self) -> *mut ff::AVPacket { + self.0 + } + + pub(crate) fn stream_index(&self) -> c_int { + unsafe { (*self.0).stream_index } + } + + pub(crate) fn unref(&mut self) { + unsafe { ff::av_packet_unref(self.0) }; + } +} + +impl Drop for OwnedPacket { + fn drop(&mut self) { + unsafe { ff::av_packet_free(&mut self.0) }; + } +} diff --git a/Explorer/Assets/Plugins/UUAV/native/src/hw_device_macos.rs b/Explorer/Assets/Plugins/UUAV/native/src/hw_device_macos.rs new file mode 100644 index 00000000000..bb59ef369d0 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/src/hw_device_macos.rs @@ -0,0 +1,126 @@ +//! VideoToolbox owns its decode pipeline end to end (no shared immediate context to race on), +//! the plugin blits on its own `MTLCommandQueue`, and `MTLDevice` itself is free-threaded. + +use anyhow::{Context as _, Result, anyhow, ensure}; +use ffmpeg_sys_next as ff; +use objc2::rc::Retained; +use objc2::runtime::ProtocolObject; +use objc2_metal::{MTLCommandQueue, MTLDevice, MTLResource as _, MTLTexture}; +use std::os::raw::c_void; +use std::ptr; + +use crate::ffutil::av_err; + +/// The engine-provided `MTLDevice`, captured once at init, plus the command +/// queue the plugin blits on. Every clone holds its own ObjC retain, so the +/// device outlives whatever the engine does with its pointer afterwards. +#[derive(Clone)] +pub(crate) struct HwDevice { + device: Retained>, + queue: Retained>, +} + +// Metal devices and command queues are free-threaded +unsafe impl Send for HwDevice {} +unsafe impl Sync for HwDevice {} + +impl HwDevice { + pub(crate) fn metal_device(&self) -> &ProtocolObject { + &self.device + } + + pub(crate) fn command_queue(&self) -> &ProtocolObject { + &self.queue + } + + /// Derives the engine device from a live `id` + /// + /// # Safety + /// `texture` must be null (rejected) or a live `id`. Unlike + /// COM there is no QueryInterface analog to probe with: a pointer to + /// anything else is undefined behavior. + pub(crate) unsafe fn from_texture(texture: *const c_void) -> Result { + ensure!(!texture.is_null(), "texture is null"); + + let texture: &ProtocolObject = unsafe { &*texture.cast() }; + let device = texture.device(); + let queue = device + .newCommandQueue() + .context("MTLDevice returned no command queue")?; + Ok(Self { device, queue }) + } +} + +/// A VideoToolbox `AVHWDeviceContext`. Unlike D3D11VA there is no engine +/// device to mirror into it: VideoToolbox needs no device handle, so +/// `av_hwdevice_ctx_create` builds the whole context itself. +pub(crate) struct HwDeviceContext { + buf: *mut ff::AVBufferRef, +} + +// AVBufferRef refcounting is atomic; VideoToolbox serializes its own +// decode sessions internally. +unsafe impl Send for HwDeviceContext {} +unsafe impl Sync for HwDeviceContext {} + +impl HwDeviceContext { + /// Takes no engine device on purpose: VideoToolbox decodes into its own + /// CVPixelBuffers, and the device only matters later, when the output + /// wraps them as textures. + pub(crate) fn new() -> Result { + let mut buf: *mut ff::AVBufferRef = ptr::null_mut(); + let ret = unsafe { + ff::av_hwdevice_ctx_create( + &mut buf, + ff::AVHWDeviceType::AV_HWDEVICE_TYPE_VIDEOTOOLBOX, + ptr::null(), + ptr::null_mut(), + 0, + ) + }; + if ret < 0 { + return Err(av_err("av_hwdevice_ctx_create(VIDEOTOOLBOX)", ret)); + } + if buf.is_null() { + return Err(anyhow!( + "av_hwdevice_ctx_create(VIDEOTOOLBOX) returned no context" + )); + } + Ok(Self { buf }) + } + + pub(crate) const fn as_buffer_ptr(&self) -> *mut ff::AVBufferRef { + self.buf + } +} + +impl Drop for HwDeviceContext { + fn drop(&mut self) { + unsafe { ff::av_buffer_unref(&mut self.buf) }; + } +} + +#[cfg(test)] +mod tests { + use ffmpeg_sys_next as ff; + use std::ptr; + + /// The decode path is dead on machines where this fails; catching it + /// here beats debugging a black video texture in the editor. + #[test] + fn videotoolbox_hwdevice_is_creatable() { + let mut buf: *mut ff::AVBufferRef = ptr::null_mut(); + let ret = unsafe { + ff::av_hwdevice_ctx_create( + &mut buf, + ff::AVHWDeviceType::AV_HWDEVICE_TYPE_VIDEOTOOLBOX, + ptr::null(), + ptr::null_mut(), + 0, + ) + }; + assert!(ret >= 0, "av_hwdevice_ctx_create(VIDEOTOOLBOX): {ret}"); + assert!(!buf.is_null()); + unsafe { ff::av_buffer_unref(&mut buf) }; + } +} diff --git a/Explorer/Assets/Plugins/UUAV/native/src/hw_device_windows.rs b/Explorer/Assets/Plugins/UUAV/native/src/hw_device_windows.rs new file mode 100644 index 00000000000..6c39926f91e --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/src/hw_device_windows.rs @@ -0,0 +1,166 @@ +use anyhow::{Context as _, Result, ensure}; +use std::os::raw::c_void; +use windows::Win32::Graphics::Direct3D11::{ID3D11Multithread, ID3D11Texture2D}; +use windows::core::IUnknown; + +use anyhow::anyhow; +use ffmpeg_sys_next as ff; +use windows::Win32::Graphics::Direct3D11::ID3D11Device; +use windows::core::Interface; + +use crate::ffutil::av_err; + +/// The engine-provided `ID3D11Device`, validated once at init. Every clone +/// holds its own COM reference, so the device outlives whatever the engine +/// does with its pointer afterwards. +#[derive(Clone)] +pub(crate) struct HwDevice { + device: ID3D11Device, +} + +// D3D11 devices are free-threaded +unsafe impl Send for HwDevice {} +unsafe impl Sync for HwDevice {} + +impl HwDevice { + pub(crate) const fn device(&self) -> &ID3D11Device { + &self.device + } + + /// Derives the engine device from a live `ID3D11Texture2D*` and captures it. + /// + /// # Safety + /// `texture` must be null (rejected) or a live COM pointer. + pub(crate) unsafe fn from_texture(texture: *const c_void) -> Result { + ensure!(!texture.is_null(), "texture is null"); + + let raw = texture.cast_mut(); + let unknown = + unsafe { IUnknown::from_raw_borrowed(&raw) }.context("texture is not a COM pointer")?; + + // QueryInterface fails cleanly on a non-D3D11 resource (e.g. D3D12) + let texture: ID3D11Texture2D = unknown + .cast() + .context("texture is not an ID3D11Texture2D (is the engine running D3D11?)")?; + + let device = unsafe { texture.GetDevice() }.context("texture has no device")?; + + // itself; enables multithread protection on the external device. + // Without it, D3D11VA decode calls on the playback thread race the engine's render + // thread on the shared immediate context and hang the runtime. + unsafe { + let immediate = device + .GetImmediateContext() + .context("device has no immediate context")?; + let multithread: ID3D11Multithread = immediate + .cast() + .context("immediate context exposes no ID3D11Multithread")?; + // the return is the previous protection state, not an error + let _ = multithread.SetMultithreadProtected(true); + } + + Ok(Self { device }) + } +} + +/// Mirror of `AVD3D11VADeviceContext` from `libavutil/hwcontext_d3d11va.h` +/// (stable public ABI; the header is not covered by the generated bindings). +/// COM pointers are kept as raw `c_void` to avoid coupling the layout to +/// `windows`-crate types. +#[repr(C)] +struct AVD3D11VADeviceContext { + device: *mut c_void, // ID3D11Device* + device_context: *mut c_void, // ID3D11DeviceContext* (immediate) + video_device: *mut c_void, // ID3D11VideoDevice* + video_context: *mut c_void, // ID3D11VideoContext* + lock: Option, + unlock: Option, + lock_ctx: *mut c_void, +} + +/// A D3D11VA `AVHWDeviceContext` bound to the externally provided +/// `ID3D11Device` (Unity's device), so decoded surfaces are directly +/// usable for GPU copies into textures sampled by Unity. +pub(crate) struct HwDeviceContext { + buf: *mut ff::AVBufferRef, +} + +// AVBufferRef refcounting is atomic; the D3D11 device is free-threaded and +// serialized through the ffmpeg-provided lock where required. +unsafe impl Send for HwDeviceContext {} +unsafe impl Sync for HwDeviceContext {} + +impl HwDeviceContext { + pub(crate) fn new(device: &HwDevice) -> Result { + unsafe { + let mut buf = ff::av_hwdevice_ctx_alloc(ff::AVHWDeviceType::AV_HWDEVICE_TYPE_D3D11VA); + if buf.is_null() { + return Err(anyhow!("av_hwdevice_ctx_alloc(D3D11VA) failed")); + } + + let dev_ctx = (*buf).data.cast::(); + let hwctx = (*dev_ctx).hwctx.cast::(); + + // av_hwdevice_ctx frees one reference on the device when the + // context is destroyed, so hand it an AddRef'd pointer + (*hwctx).device = device.device().clone().into_raw(); + + let ret = ff::av_hwdevice_ctx_init(buf); + if ret < 0 { + ff::av_buffer_unref(&mut buf); + return Err(av_err("av_hwdevice_ctx_init(D3D11VA)", ret)); + } + + Ok(Self { buf }) + } + } + + pub(crate) const fn as_buffer_ptr(&self) -> *mut ff::AVBufferRef { + self.buf + } + + fn hwctx(&self) -> *mut AVD3D11VADeviceContext { + unsafe { + let dev_ctx = (*self.buf).data.cast::(); + (*dev_ctx).hwctx.cast::() + } + } + + /// The immediate `ID3D11DeviceContext*` owned by the hw context. + /// Only touch it while holding [`Self::lock`] — the decoder threads + /// of FFmpeg use the same context under the same lock. + pub(crate) fn immediate_context_raw(&self) -> *mut c_void { + unsafe { (*self.hwctx()).device_context } + } + + pub(crate) fn lock(&self) -> HwLockGuard<'_> { + unsafe { + let hwctx = self.hwctx(); + if let Some(lock) = (*hwctx).lock { + lock((*hwctx).lock_ctx); + } + } + HwLockGuard { ctx: self } + } +} + +impl Drop for HwDeviceContext { + fn drop(&mut self) { + unsafe { ff::av_buffer_unref(&mut self.buf) }; + } +} + +pub(crate) struct HwLockGuard<'a> { + ctx: &'a HwDeviceContext, +} + +impl Drop for HwLockGuard<'_> { + fn drop(&mut self) { + unsafe { + let hwctx = self.ctx.hwctx(); + if let Some(unlock) = (*hwctx).unlock { + unlock((*hwctx).lock_ctx); + } + } + } +} diff --git a/Explorer/Assets/Plugins/UUAV/native/src/lib.rs b/Explorer/Assets/Plugins/UUAV/native/src/lib.rs new file mode 100644 index 00000000000..4909fa5ace5 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/src/lib.rs @@ -0,0 +1,976 @@ +#![warn(clippy::all, clippy::pedantic, clippy::nursery)] +#![deny( + clippy::unwrap_used, + clippy::expect_used, + clippy::panic, + clippy::indexing_slicing, + clippy::arithmetic_side_effects, + clippy::todo, + clippy::dbg_macro +)] +#![allow( + clippy::uninlined_format_args, + clippy::missing_errors_doc, + clippy::option_if_let_else, + clippy::single_match_else, + clippy::must_use_candidate, + clippy::future_not_send, + clippy::enum_glob_use +)] +// FFI-heavy crate: raw-pointer borrows, numeric casts between C and media +// timestamp domains, and crate-private modules are pervasive by nature +// (doc_markdown: insists on backticks around the word "FFmpeg" in prose) +#![allow( + clippy::borrow_as_ptr, + clippy::redundant_pub_crate, + clippy::cast_precision_loss, + clippy::cast_sign_loss, + clippy::cast_possible_truncation, + clippy::cast_ptr_alignment, + clippy::missing_safety_doc, + clippy::doc_markdown +)] + +mod audio_decoder; +mod ffutil; +mod playback; +mod player; + +// Per-platform sibling modules: same names, same cross-platform-consumed +// surface, selected by target. Consumers stay platform-agnostic. +#[cfg(target_os = "windows")] +#[path = "hw_device_windows.rs"] +mod hw_device; +#[cfg(target_os = "macos")] +#[path = "hw_device_macos.rs"] +mod hw_device; + +#[cfg(target_os = "windows")] +#[path = "video_decoder_windows.rs"] +mod video_decoder; +#[cfg(target_os = "macos")] +#[path = "video_decoder_macos.rs"] +mod video_decoder; + +#[cfg(target_os = "windows")] +#[path = "video_output_windows.rs"] +mod video_output; +#[cfg(target_os = "macos")] +#[path = "video_output_macos.rs"] +mod video_output; + +#[cfg(not(any(target_os = "windows", target_os = "macos")))] +compile_error!("uuav supports Windows (D3D11) and macOS (Metal) only"); + +use anyhow::{Context as _, ensure}; +use arc_swap::{ArcSwap, ArcSwapOption}; +use dashmap::DashMap; +use ffmpeg_sys_next as ff; +use ffutil::StreamingProtocol; +use hw_device::HwDevice; +use playback::DEFAULT_PLAYBACK_RATE; +use player::UUAVPlayer; +use std::{ + convert::AsRef, + ffi::{CStr, CString}, + num::{NonZeroI32, NonZeroUsize}, + os::raw::{c_char, c_int, c_void}, + ptr, + sync::{Arc, Once, Weak, atomic::AtomicU64}, +}; + +const ERR_NO_RUNTIME: &str = "Runtime is not found"; +const ERR_NO_PLAYER: &str = "player with specific id not found"; + +static INIT_STATE: ArcSwapOption = ArcSwapOption::const_empty(); +static NEXT_STREAM_ID: AtomicU64 = AtomicU64::new(1); + +// installs the FFmpeg log trampoline exactly once, process-wide +static LOG_INIT: Once = Once::new(); + +// FFmpeg `lavu_log_constants`; local copies avoid depending on binding names. +// Messages at or below these levels route to the matching Unity sink. +const AV_LOG_ERROR: c_int = 16; +const AV_LOG_WARNING: c_int = 24; + +// Stack buffer for one formatted FFmpeg log line (prefix + message). +const LOG_LINE_CAP: c_int = 1024; + +struct Runtime { + device: HwDevice, + error_callback: Arc, + warning_callback: Arc, + log_callback: Arc, + audio_options: Arc>, + protocol_whitelist: Arc, + registry: DashMap, +} + +type RawErrorCallback = extern "C" fn(*const c_char); + +// FFmpeg log sinks share the error-callback shape: a single NUL-terminated, +// already-formatted line. Level selects which sink native routes to. +type RawLogCallback = extern "C" fn(*const c_char); + +// no-op when the raw callback is dropped on deinit +#[derive(Clone)] +struct ErrorCallback { + callback: Weak, +} + +impl ErrorCallback { + pub fn from_raw(callback: Weak) -> Self { + Self { callback } + } + + pub fn report(&self, message: impl AsRef) { + if let Some(callback) = self.callback.upgrade() { + let message: &str = message.as_ref(); + let c = CString::new(message).unwrap_or_default(); + callback(c.as_ptr()); + } + } +} + +pub type PlayerId = u64; + +#[allow(non_camel_case_types)] +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub enum UUAVState { + UUAV_CLOSED = 0, + UUAV_OPENING = 1, + UUAV_READY = 2, + UUAV_PLAYING = 3, + UUAV_PAUSED = 4, + UUAV_ENDED = 5, + UUAV_ERROR = 6, + UUAV_UNKNOWN = 7, +} + +#[repr(C)] +#[derive(Default)] +pub struct Status { + pub players_count: u64, + pub initialized: bool, + pub audio_options: AudioOptionsRaw, + pub device_remove_reason: *const c_char, +} + +/// Untrusted external input, must never be used for internals. +/// Always convert to AudioOptions to sanitize. +#[repr(C)] +#[derive(Default, Clone, Copy, PartialEq, Eq)] +pub struct AudioOptionsRaw { + pub sample_rate: i32, + pub channels: i32, +} + +/// Sanitized input, guarantees to have values greater than zero +#[repr(C)] +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct AudioOptions { + pub sample_rate: NonZeroI32, + pub channels: NonZeroI32, +} + +impl AudioOptions { + // AudioOptions is sanitized at the FFI boundary: always positive + const fn channels_usize(self) -> NonZeroUsize { + // SAFETY the channels value is checked at the construction + unsafe { NonZeroUsize::new_unchecked(self.channels.get() as usize) } + } + + fn sample_rate_f64(self) -> f64 { + f64::from(self.sample_rate.get()) + } +} + +impl TryFrom for AudioOptions { + type Error = anyhow::Error; + + fn try_from(raw: AudioOptionsRaw) -> Result { + ensure!( + raw.sample_rate > 0, + "sample_rate must be positive, got {}", + raw.sample_rate + ); + ensure!( + raw.channels > 0, + "channels must be positive, got {}", + raw.channels + ); + + unsafe { + let sample_rate = NonZeroI32::new_unchecked(raw.sample_rate); + let channels = NonZeroI32::new_unchecked(raw.channels); + Ok(Self { + sample_rate, + channels, + }) + } + } +} + +impl From for AudioOptionsRaw { + fn from(options: AudioOptions) -> Self { + Self { + sample_rate: options.sample_rate.get(), + channels: options.channels.get(), + } + } +} + +/// Read-only view of the engine's audio output configuration +/// For internal look-ups +#[derive(Clone)] +pub(crate) struct AudioOptionsView(Arc>); + +impl AudioOptionsView { + pub(crate) fn current(&self) -> AudioOptions { + **self.0.load() + } +} + +#[repr(C)] +#[derive(Clone)] +pub struct VideoSize { + pub width: u32, + pub height: u32, +} + +/// Snapshot of the open media's source stream parameters. +/// +/// Name fields are NUL-terminated UTF-8; = +/// unknown values are 0 / empty, `duration` is -1.0 for realtime streams. +/// Video fields are zero when `has_video` is 0, +/// audio fields when `has_audio` is 0. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct MediaInfo { + pub duration: f64, + pub framerate: f64, + pub video_bitrate: i64, + pub audio_bitrate: i64, + pub width: u32, + pub height: u32, + pub sample_rate: i32, + pub channels: i32, + pub video_codec: [c_char; MEDIA_INFO_NAME_LEN], + pub pixel_format: [c_char; MEDIA_INFO_NAME_LEN], + pub audio_codec: [c_char; MEDIA_INFO_NAME_LEN], + pub sample_format: [c_char; MEDIA_INFO_NAME_LEN], + pub has_video: u8, + pub has_audio: u8, +} + +pub const MEDIA_INFO_NAME_LEN: usize = 32; + +impl MediaInfo { + pub(crate) const fn empty() -> Self { + Self { + duration: -1.0, + framerate: 0.0, + video_bitrate: 0, + audio_bitrate: 0, + width: 0, + height: 0, + sample_rate: 0, + channels: 0, + video_codec: [0; MEDIA_INFO_NAME_LEN], + pixel_format: [0; MEDIA_INFO_NAME_LEN], + audio_codec: [0; MEDIA_INFO_NAME_LEN], + sample_format: [0; MEDIA_INFO_NAME_LEN], + has_video: 0, + has_audio: 0, + } + } +} + +/// Snapshot of the user-facing control values: the latest pushed +/// intents, which the playback thread applies asynchronously. +/// +/// A `*_pending` flag of 1 means that control's latest push has not +/// been consumed by the playback thread yet. Each value/pending pair +/// is consistent; the controls are sampled independently of each other. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct ControlsState { + pub rate: f64, + pub play: u8, + pub play_pending: u8, + pub looping: u8, + pub looping_pending: u8, + pub rate_pending: u8, +} + +#[repr(C)] +pub struct NewPlayerResult { + pub player_id: PlayerId, + pub error_message: *const c_char, +} + +impl NewPlayerResult { + const fn ok(player_id: PlayerId) -> Self { + Self { + player_id, + error_message: ptr::null(), + } + } + + fn error(message: impl AsRef) -> Self { + Self { + player_id: 0, + error_message: string_to_c_bytes(message), + } + } +} + +#[repr(C)] +pub struct ResultFFI { + pub error_message: *const c_char, +} + +impl ResultFFI { + const fn ok() -> Self { + Self { + error_message: ptr::null(), + } + } + + fn error(message: &str) -> Self { + Self { + error_message: string_to_c_bytes(message), + } + } +} + +impl From> for ResultFFI { + fn from(value: anyhow::Result) -> Self { + match value { + Ok(_) => Self::ok(), + Err(e) => Self::error(e.to_string().as_str()), + } + } +} + +impl From for ResultFFI { + fn from(err: anyhow::Error) -> Self { + Self::error(err.to_string().as_str()) + } +} + +fn string_to_c_bytes(s: impl AsRef) -> *const c_char { + CString::new(s.as_ref()).unwrap_or_default().into_raw() +} + +impl Runtime { + /// Shared-access guard over a registered player; the player stays + /// solely owned by the registry, so the guard cannot outlive it. + fn player_by_id( + &self, + player_id: PlayerId, + ) -> anyhow::Result> { + self.registry.get(&player_id).context(ERR_NO_PLAYER) + } + + /// Exclusive-access counterpart of [`Self::player_by_id`]. + fn player_by_id_mut( + &self, + player_id: PlayerId, + ) -> anyhow::Result> { + self.registry.get_mut(&player_id).context(ERR_NO_PLAYER) + } +} + +/// releases an error message +/// must be called exactly once per non-null message +#[unsafe(no_mangle)] +pub unsafe extern "C" fn uuav_string_free(string: *mut c_char) { + if string.is_null() { + return; + } + + drop(unsafe { CString::from_raw(string) }); +} + +#[unsafe(no_mangle)] +pub const extern "C" fn uuav_abi_version() -> *const c_char { + concat!(env!("CARGO_PKG_VERSION"), '\0').as_ptr().cast() +} + +/// Process-global FFmpeg log callback installed via `av_log_set_callback`. +/// +/// Fires from arbitrary (possibly concurrent) FFmpeg threads. It no-ops once +/// the runtime is torn down, applies the configured verbosity threshold itself +/// (FFmpeg leaves that to the callback), formats the line — including the +/// `AVClass`-derived `[component @ 0x..]` prefix — via `av_log_format_line2`, +/// then routes to the Unity sink matching the severity. `vl` is this target's +/// `va_list` (a plain `*mut c_char` on `x86_64-pc-windows-gnu`). +unsafe extern "C" fn uuav_ffmpeg_log( + avcl: *mut c_void, + level: c_int, + fmt: *const c_char, + vl: *mut c_char, +) { + let state = INIT_STATE.load(); + let Some(s) = state.as_ref() else { + return; + }; + + // FFmpeg only stores the threshold; filtering is the callback's job. + if level > unsafe { ff::av_log_get_level() } { + return; + } + + let mut line = [0 as c_char; LOG_LINE_CAP as usize]; + let mut print_prefix: c_int = 1; + unsafe { + ff::av_log_format_line2( + avcl, + level, + fmt, + vl, + line.as_mut_ptr(), + LOG_LINE_CAP, + &mut print_prefix, + ); + } + + let callback = if level <= AV_LOG_ERROR { + &s.error_callback + } else if level <= AV_LOG_WARNING { + &s.warning_callback + } else { + &s.log_callback + }; + // av_log_format_line2 always NUL-terminates within the buffer. + callback(line.as_ptr()); +} + +/// Sets the FFmpeg verbosity threshold (an `AV_LOG_*` constant). Messages above +/// this level are dropped before reaching any Unity sink. +#[unsafe(no_mangle)] +pub extern "C" fn uuav_set_log_level(level: c_int) { + unsafe { ff::av_log_set_level(level) }; +} + +/// Initializes the global runtime. +/// +/// `protocol_whitelist` is a NUL-terminated, comma-separated FFmpeg protocol +/// list; init fails if it is null or empty. Recommended baseline is +/// `https,http,tls,tcp,crypto,data,udp,rtp,rtcp,rtsp`, adding `file` only for +/// editor/local playback. +/// +/// `warning_callback` and `log_callback` receive FFmpeg's own diagnostics +/// (already formatted, one line per call); `log_level` is the initial +/// `AV_LOG_*` verbosity threshold. All three callbacks must be non-null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn uuav_init( + texture: *const c_void, + audio_options: AudioOptionsRaw, + error_callback: Option, + warning_callback: Option, + log_callback: Option, + protocol_whitelist: *const c_char, + log_level: c_int, +) -> ResultFFI { + if INIT_STATE.load().is_some() { + return ResultFFI::error("Already initialized"); + } + + let Some(error_callback) = error_callback else { + return ResultFFI::error("Error callback is null"); + }; + + let Some(warning_callback) = warning_callback else { + return ResultFFI::error("Warning callback is null"); + }; + + let Some(log_callback) = log_callback else { + return ResultFFI::error("Log callback is null"); + }; + + if texture.is_null() { + return ResultFFI::error("Texture to capture the HwDevice from is not provided"); + } + + let device = match unsafe { hw_device::HwDevice::from_texture(texture) } { + Ok(hw_device) => hw_device, + Err(e) => return e.into(), + }; + + let audio_options = match AudioOptions::try_from(audio_options) { + Ok(options) => options, + Err(e) => return e.into(), + }; + + let protocol_whitelist = match unsafe { StreamingProtocol::new(protocol_whitelist) } { + Ok(protocols) => protocols, + Err(e) => return e.into(), + }; + + let new_runtime = Runtime { + device, + error_callback: Arc::new(error_callback), + warning_callback: Arc::new(warning_callback), + log_callback: Arc::new(log_callback), + audio_options: Arc::new(ArcSwap::new(Arc::new(audio_options))), + protocol_whitelist: Arc::new(protocol_whitelist), + registry: DashMap::new(), + }; + + INIT_STATE.store(Some(Arc::new(new_runtime))); + + // Register the trampoline once; the sinks live in INIT_STATE, so it no-ops + // after deinit and picks up fresh callbacks on re-init. The level store is + // process-global, so re-apply it every init. + LOG_INIT.call_once(|| unsafe { ff::av_log_set_callback(Some(uuav_ffmpeg_log)) }); + unsafe { ff::av_log_set_level(log_level) }; + + ResultFFI::ok() +} + +#[unsafe(no_mangle)] +pub extern "C" fn uuav_deinit() { + INIT_STATE.store(None); +} + +#[unsafe(no_mangle)] +pub extern "C" fn uuav_update_audio_out(options: AudioOptionsRaw) -> ResultFFI { + let state = INIT_STATE.load(); + + let Some(s) = state.as_ref() else { + return ResultFFI::error("Not initialized"); + }; + + match AudioOptions::try_from(options) { + Ok(options) => { + s.audio_options.store(Arc::new(options)); + ResultFFI::ok() + } + Err(e) => ResultFFI::error(&e.to_string()), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn uuav_status() -> Status { + if let Some(s) = INIT_STATE.load().as_ref() { + let audio_options = (**s.audio_options.load()).into(); + let players_count = s.registry.len() as u64; + + // D3D11 devices can be removed (driver reset, GPU hang) + // Metal has no such concept + #[cfg(target_os = "windows")] + let device_remove_reason = match unsafe { s.device.device().GetDeviceRemovedReason() } { + Ok(()) => ptr::null(), + Err(e) => string_to_c_bytes(e.to_string()), + }; + #[cfg(target_os = "macos")] + let device_remove_reason = ptr::null(); + + Status { + players_count, + initialized: true, + audio_options, + device_remove_reason, + } + } else { + Status::default() + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn uuav_player_new() -> NewPlayerResult { + let state = INIT_STATE.load(); + let Some(s) = state.as_ref() else { + return NewPlayerResult::error(ERR_NO_RUNTIME); + }; + + let next_id = NEXT_STREAM_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + match UUAVPlayer::new( + next_id, + s.device.clone(), + AudioOptionsView(Arc::clone(&s.audio_options)), + ErrorCallback::from_raw(Arc::downgrade(&s.error_callback)), + Arc::clone(&s.protocol_whitelist), + ) { + Ok(player) => { + s.registry.insert(next_id, player); + NewPlayerResult::ok(next_id) + } + Err(e) => NewPlayerResult::error(e.to_string()), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn uuav_player_free(player_id: PlayerId) { + let state = INIT_STATE.load(); + let Some(s) = state.as_ref() else { + return; + }; + + s.registry.remove(&player_id); +} + +// ---- lifecycle ------------------------------------------------------- + +#[unsafe(no_mangle)] +pub extern "C" fn uuav_player_play(player_id: PlayerId) -> ResultFFI { + uuav_player_play_internal(player_id).into() +} + +fn uuav_player_play_internal(player_id: PlayerId) -> anyhow::Result<()> { + let state = INIT_STATE.load(); + let runtime = state.as_ref().context(ERR_NO_RUNTIME)?; + runtime.player_by_id(player_id)?.play() +} + +#[unsafe(no_mangle)] +pub extern "C" fn uuav_player_pause(player_id: PlayerId) -> ResultFFI { + uuav_player_pause_internal(player_id).into() +} + +fn uuav_player_pause_internal(player_id: PlayerId) -> anyhow::Result<()> { + let state = INIT_STATE.load(); + let runtime = state.as_ref().context(ERR_NO_RUNTIME)?; + runtime.player_by_id(player_id)?.pause() +} + +// async! returns immediately +#[unsafe(no_mangle)] +pub unsafe extern "C" fn uuav_player_open_media_async( + player_id: PlayerId, + url: *const c_char, +) -> ResultFFI { + unsafe { uuav_player_open_media_async_internal(player_id, url) }.into() +} + +unsafe fn uuav_player_open_media_async_internal( + player_id: PlayerId, + url: *const c_char, +) -> anyhow::Result<()> { + ensure!(!url.is_null(), "url is null"); + let url = unsafe { CStr::from_ptr(url) } + .to_str() + .context("url is not valid UTF-8")? + .to_owned(); + let state = INIT_STATE.load(); + let runtime = state.as_ref().context(ERR_NO_RUNTIME)?; + runtime.player_by_id_mut(player_id)?.open_media_intent(url) +} + +// back to CLOSED, player reusable +#[unsafe(no_mangle)] +pub extern "C" fn uuav_player_close_media(player_id: PlayerId) -> ResultFFI { + uuav_player_close_media_internal(player_id).into() +} + +fn uuav_player_close_media_internal(player_id: PlayerId) -> anyhow::Result<()> { + let state = INIT_STATE.load(); + let runtime = state.as_ref().context(ERR_NO_RUNTIME)?; + runtime.player_by_id_mut(player_id)?.close_media(); + Ok(()) +} + +#[unsafe(no_mangle)] +pub extern "C" fn uuav_player_state(player_id: PlayerId) -> UUAVState { + let state = INIT_STATE.load(); + let Some(runtime) = state.as_ref() else { + return UUAVState::UUAV_UNKNOWN; + }; + + runtime + .player_by_id(player_id) + .map_or(UUAVState::UUAV_UNKNOWN, |player| player.state()) +} + +// may be unavailable for realtime streams +#[unsafe(no_mangle)] +pub unsafe extern "C" fn uuav_player_duration( + player_id: PlayerId, + out_duration: *mut f64, +) -> ResultFFI { + unsafe { uuav_player_duration_internal(player_id, out_duration) }.into() +} + +unsafe fn uuav_player_duration_internal( + player_id: PlayerId, + out_duration: *mut f64, +) -> anyhow::Result<()> { + ensure!(!out_duration.is_null(), "out pointer is null"); + let state = INIT_STATE.load(); + let runtime = state.as_ref().context(ERR_NO_RUNTIME)?; + let duration = runtime + .player_by_id(player_id)? + .duration() + .context("duration is not available")?; + unsafe { out_duration.write(duration) }; + Ok(()) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn uuav_player_current_controls_state( + player_id: PlayerId, + out_state: *mut ControlsState, +) -> ResultFFI { + unsafe { uuav_player_current_controls_state_internal(player_id, out_state) }.into() +} + +unsafe fn uuav_player_current_controls_state_internal( + player_id: PlayerId, + out_state: *mut ControlsState, +) -> anyhow::Result<()> { + ensure!(!out_state.is_null(), "out pointer is null"); + let state = INIT_STATE.load(); + let runtime = state.as_ref().context(ERR_NO_RUNTIME)?; + let controls = runtime.player_by_id(player_id)?.controls_state(); + unsafe { out_state.write(controls) }; + Ok(()) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn uuav_player_current_time( + player_id: PlayerId, + out_time: *mut f64, +) -> ResultFFI { + unsafe { uuav_player_current_time_internal(player_id, out_time) }.into() +} + +unsafe fn uuav_player_current_time_internal( + player_id: PlayerId, + out_time: *mut f64, +) -> anyhow::Result<()> { + ensure!(!out_time.is_null(), "out pointer is null"); + let state = INIT_STATE.load(); + let runtime = state.as_ref().context(ERR_NO_RUNTIME)?; + let time = runtime + .player_by_id(player_id)? + .current_time() + .context("current time is not available")?; + unsafe { out_time.write(time) }; + Ok(()) +} + +// the player slaves its playback to the externally provided master clock +#[unsafe(no_mangle)] +pub extern "C" fn uuav_player_assign_master_clock( + player_id: PlayerId, + current_time: f64, +) -> ResultFFI { + uuav_player_assign_master_clock_internal(player_id, current_time).into() +} + +fn uuav_player_assign_master_clock_internal( + player_id: PlayerId, + current_time: f64, +) -> anyhow::Result<()> { + let state = INIT_STATE.load(); + let runtime = state.as_ref().context(ERR_NO_RUNTIME)?; + runtime + .player_by_id(player_id)? + .assign_master_clock(current_time) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn uuav_player_get_video_size( + player_id: PlayerId, + out_size: *mut VideoSize, +) -> ResultFFI { + unsafe { uuav_player_get_video_size_internal(player_id, out_size) }.into() +} + +unsafe fn uuav_player_get_video_size_internal( + player_id: PlayerId, + out_size: *mut VideoSize, +) -> anyhow::Result<()> { + ensure!(!out_size.is_null(), "out pointer is null"); + let state = INIT_STATE.load(); + let runtime = state.as_ref().context(ERR_NO_RUNTIME)?; + let size = runtime + .player_by_id(player_id)? + .video_size() + .context("video size is not available yet")?; + unsafe { out_size.write(size) }; + Ok(()) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn uuav_player_get_media_info( + player_id: PlayerId, + out_info: *mut MediaInfo, +) -> ResultFFI { + unsafe { uuav_player_get_media_info_internal(player_id, out_info) }.into() +} + +unsafe fn uuav_player_get_media_info_internal( + player_id: PlayerId, + out_info: *mut MediaInfo, +) -> anyhow::Result<()> { + ensure!(!out_info.is_null(), "out pointer is null"); + let state = INIT_STATE.load(); + let runtime = state.as_ref().context(ERR_NO_RUNTIME)?; + let info = runtime + .player_by_id(player_id)? + .media_info() + .context("media info is not available yet")?; + unsafe { out_info.write(info) }; + Ok(()) +} + +// ---- transport (commands: set flags, decoder thread obeys) ---------- + +// async; coalesces repeated calls +#[unsafe(no_mangle)] +pub extern "C" fn uuav_player_seek_async(player_id: PlayerId, time: f64) -> ResultFFI { + uuav_player_seek_async_internal(player_id, time).into() +} + +fn uuav_player_seek_async_internal(player_id: PlayerId, time: f64) -> anyhow::Result<()> { + let state = INIT_STATE.load(); + let runtime = state.as_ref().context(ERR_NO_RUNTIME)?; + runtime.player_by_id(player_id)?.seek_intent(time) +} + +// persists across url switches +#[unsafe(no_mangle)] +pub extern "C" fn uuav_player_set_looping(player_id: PlayerId, looping: u8) -> ResultFFI { + uuav_player_set_looping_internal(player_id, looping != 0).into() +} + +fn uuav_player_set_looping_internal(player_id: PlayerId, looping: bool) -> anyhow::Result<()> { + let state = INIT_STATE.load(); + let runtime = state.as_ref().context(ERR_NO_RUNTIME)?; + runtime.player_by_id(player_id)?.set_looping(looping); + Ok(()) +} + +#[unsafe(no_mangle)] +pub extern "C" fn uuav_player_get_looping(player_id: PlayerId) -> u8 { + let state = INIT_STATE.load(); + let Some(runtime) = state.as_ref() else { + return 0; + }; + runtime + .player_by_id(player_id) + .map_or(0, |player| u8::from(player.looping())) +} + +// Expect: realtime streams (no duration) keep playing at 1x +#[unsafe(no_mangle)] +pub extern "C" fn uuav_player_set_rate(player_id: PlayerId, rate: f64) -> ResultFFI { + uuav_player_set_rate_internal(player_id, rate).into() +} + +fn uuav_player_set_rate_internal(player_id: PlayerId, rate: f64) -> anyhow::Result<()> { + ensure!( + rate.is_finite() && rate > 0.0, + "playback rate must be finite and positive, got {rate}" + ); + let state = INIT_STATE.load(); + let runtime = state.as_ref().context(ERR_NO_RUNTIME)?; + runtime.player_by_id(player_id)?.set_rate(rate); + Ok(()) +} + +#[unsafe(no_mangle)] +pub extern "C" fn uuav_player_get_rate(player_id: PlayerId) -> f64 { + let state = INIT_STATE.load(); + let Some(runtime) = state.as_ref() else { + return DEFAULT_PLAYBACK_RATE; + }; + runtime + .player_by_id(player_id) + .map_or(DEFAULT_PLAYBACK_RATE, |player| player.rate()) +} + +// ---- video ----------------------------------------------------------- + +// Unity's UnityRenderingEvent signature +pub type UUAVRenderEvent = extern "C" fn(event_id: i32); + +// [render] entry point issued via GL.IssuePluginEvent; +// `event_id` routes to the player with the matching id +extern "C" fn uuav_render_event(event_id: i32) { + let Ok(player_id) = PlayerId::try_from(event_id) else { + return; + }; + + let state = INIT_STATE.load(); + let Some(runtime) = state.as_ref() else { + return; + }; + + if let Ok(player) = runtime.player_by_id(player_id) { + player.on_render_event(); + } +} + +// pass to GL.IssuePluginEvent +#[unsafe(no_mangle)] +pub extern "C" fn uuav_get_render_callback() -> UUAVRenderEvent { + uuav_render_event +} + +// Valid from the first presented frame; what the pointer is per platform +// and when it invalidates is documented on `video_output::VideoTextureView`. +// `plane` is part of the fixed C ABI; only Metal consumes it. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn uuav_player_get_video_texture( + player_id: PlayerId, + #[cfg_attr(target_os = "windows", allow(unused_variables))] plane: i32, + out_texture: *mut *const c_void, +) -> ResultFFI { + unsafe { + uuav_player_get_video_texture_internal( + player_id, + #[cfg(target_os = "macos")] + plane, + out_texture, + ) + } + .into() +} + +unsafe fn uuav_player_get_video_texture_internal( + player_id: PlayerId, + #[cfg(target_os = "macos")] plane: i32, + out_texture: *mut *const c_void, +) -> anyhow::Result<()> { + ensure!(!out_texture.is_null(), "out pointer is null"); + let state = INIT_STATE.load(); + let runtime = state.as_ref().context(ERR_NO_RUNTIME)?; + let texture = runtime + .player_by_id(player_id)? + .video_texture( + #[cfg(target_os = "macos")] + plane, + ) + .context("video texture is not available yet")?; + unsafe { out_texture.write(texture.raw_ptr_mut().cast_const()) }; + Ok(()) +} + +// ---- audio ----------------------------------------------------------- + +// [audio] fills interleaved FLT; pads silence on underrun, +// never blocks; returns frames actually copied +#[unsafe(no_mangle)] +pub unsafe extern "C" fn uuav_player_read_audio( + player_id: PlayerId, + dst: *mut f32, + nb_frames: i32, +) -> i32 { + if dst.is_null() || nb_frames <= 0 { + return 0; + } + + let state = INIT_STATE.load(); + let Some(runtime) = state.as_ref() else { + return 0; + }; + + runtime + .player_by_id(player_id) + .map_or(0, |player| player.read_audio(dst, nb_frames)) +} diff --git a/Explorer/Assets/Plugins/UUAV/native/src/playback/audio_playback.rs b/Explorer/Assets/Plugins/UUAV/native/src/playback/audio_playback.rs new file mode 100644 index 00000000000..2838d23908f --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/src/playback/audio_playback.rs @@ -0,0 +1,192 @@ +use anyhow::Result; +use parking_lot::Mutex; +use std::os::raw::c_int; +use std::ptr; +use std::sync::Arc; +use std::thread; + +use super::audio_ring::{AudioRing, ClockSync, SharedAudioReceiver}; +use super::fill_silence; +use super::transport::AtomicTransport; +use super::util::{AtomicSeekSlot, PLAYBACK_POLL, ReadOnlyCancelToken}; +use crate::AudioOptionsView; +use crate::audio_decoder::AudioDecoder; +use crate::ffutil::{Decoded, OwnedPacket, Stream}; + +/// The audio-thread half of a playback's audio: drains the ring +/// [`AudioPlayback`] fills, slaved to the master clock. +pub(super) struct AudioReader { + /// Receiver half of the decoded-audio ring; `None` until the audio + /// decoder reports its format. The mutex is uncontended in steady + /// state: the worker takes it only to swap in a fresh receiver. + rx: SharedAudioReceiver, + /// Engine audio configuration; the silence layout when no decoded + /// format exists yet. + audio_out: AudioOptionsView, +} + +impl AudioReader { + pub(super) fn new(audio_out: AudioOptionsView) -> Self { + Self { + rx: Arc::new(Mutex::new(None)), + audio_out, + } + } + + /// The slot [`AudioPlayback`] publishes its receiver halves into. + pub(super) fn rx_slot(&self) -> SharedAudioReceiver { + Arc::clone(&self.rx) + } + + /// [audio thread] Fills interleaved FLT; pads silence on underrun, + /// never blocks; returns frames actually copied. + pub(super) fn read(&self, transport: &AtomicTransport, dst: *mut f32, frames: usize) -> i32 { + let mut guard = self.rx.lock(); + let Some(ring) = guard.as_mut() else { + // no decoded format yet: silence in the engine's configured layout + fill_silence(dst, frames, self.audio_out.current().channels_usize()); + return 0; + }; + + let Some(total) = frames.checked_mul(ring.channel_count().get()) else { + return 0; + }; + // the engine guarantees dst holds nb_frames * channels floats + let out = unsafe { std::slice::from_raw_parts_mut(dst, total) }; + + if !transport.is_playing() { + out.fill(0.0); + return 0; + } + + match ring.sync_to_clock(transport.now()) { + ClockSync::EmitSilence => { + out.fill(0.0); + 0 + } + ClockSync::Consume => i32::try_from(ring.read_into(out)).unwrap_or(0), + } + } +} + +/// The worker-thread half of a playback's audio: decodes the audio stream +/// and feeds the ring the engine's audio thread drains. +pub(super) struct AudioPlayback { + decoder: AudioDecoder, + ring: AudioRing, + audio_out: AudioOptionsView, + stream_index: c_int, + /// Varispeed currently applied; new rings must carry it so their + /// media-time math matches the converted samples. + playback_rate: f64, +} + +impl AudioPlayback { + pub(super) fn new( + stream: Stream, + stream_index: c_int, + audio_out: AudioOptionsView, + rx_slot: SharedAudioReceiver, + ) -> Result { + let decoder = AudioDecoder::new(stream, audio_out.current())?; + let playback_rate = super::unit::DEFAULT_PLAYBACK_RATE; + let ring = AudioRing::new(decoder.audio_options(), playback_rate, rx_slot); + Ok(Self { + decoder, + ring, + audio_out, + stream_index, + playback_rate, + }) + } + + /// Applies a new varispeed rate: rebuilds the resampler ratio and + /// drops already-converted samples (same policy as an audio reconfig). + pub(super) fn set_rate(&mut self, rate: f64) { + if self.decoder.set_rate(rate) { + self.playback_rate = rate; + self.ring.replace(self.decoder.audio_options(), rate); + } + } + + pub(super) const fn handles(&self, stream_index: c_int) -> bool { + self.stream_index == stream_index + } + + /// Sends the packet and moves every ready frame into the ring, + /// following engine audio reconfiguration (`uuav_update_audio_out`). + pub(super) fn handle_packet( + &mut self, + packet: &mut OwnedPacket, + start_offset: f64, + cancel: &ReadOnlyCancelToken, + seek: &AtomicSeekSlot, + poll_controls: &dyn Fn(), + ) -> Result<()> { + let options = self.audio_out.current(); + if self.decoder.set_output(options) { + self.ring.replace(options, self.playback_rate); + } + self.decoder.send(packet.as_mut_ptr())?; + self.pump(start_offset, cancel, seek, poll_controls) + } + + /// Sends end-of-stream and moves the remaining samples into the ring. + pub(super) fn drain( + &mut self, + start_offset: f64, + cancel: &ReadOnlyCancelToken, + seek: &AtomicSeekSlot, + poll_controls: &dyn Fn(), + ) -> Result<()> { + self.decoder.send(ptr::null())?; + self.pump(start_offset, cancel, seek, poll_controls) + } + + /// Discards everything belonging to the pre-seek position. + pub(super) fn flush_for_seek(&mut self) { + self.decoder.flush(); + self.ring + .replace(self.decoder.audio_options(), self.playback_rate); + } + + /// Whether the audio thread has consumed every pushed sample. + pub(super) fn is_drained(&self) -> bool { + self.ring.is_drained() + } + + /// Moves every frame the decoder has ready into the ring, waiting for + /// room while staying responsive to stop/seek commands. + /// The wait also keeps `poll_controls` applied: while not playing the + /// ring does not drain, so a queued play would otherwise never land. + fn pump( + &mut self, + start_offset: f64, + cancel: &ReadOnlyCancelToken, + seek: &AtomicSeekSlot, + poll_controls: &dyn Fn(), + ) -> Result<()> { + loop { + match self.decoder.receive()? { + Decoded::Frame(frame) => { + let pts = frame.pts().map(|pts| pts - start_offset); + loop { + if cancel.is_cancelled() || seek.is_pending() { + // the frame is obsolete: the ring is about to + // be flushed + return Ok(()); + } + // after the cancel check: a retired unit must not + // consume a command meant for its successor + poll_controls(); + if self.ring.try_extend(pts, frame.samples()) { + break; + } + thread::sleep(PLAYBACK_POLL); + } + } + Decoded::Again | Decoded::Eof => return Ok(()), + } + } + } +} diff --git a/Explorer/Assets/Plugins/UUAV/native/src/playback/audio_ring.rs b/Explorer/Assets/Plugins/UUAV/native/src/playback/audio_ring.rs new file mode 100644 index 00000000000..958d5587673 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/src/playback/audio_ring.rs @@ -0,0 +1,244 @@ +//! Lock-free SPSC audio ring + +use parking_lot::Mutex; +use ringbuf::traits::{Consumer, Observer, Producer, Split}; +use ringbuf::{HeapCons, HeapProd, HeapRb}; +use std::num::NonZeroUsize; +use std::sync::Arc; + +use crate::AudioOptions; + +/// Decoded audio buffered ahead of the clock, in seconds; the fixed +/// capacity of the sample ring. +const AUDIO_BUFFER_SECONDS: f64 = 1.0; +/// Audio drift beyond this is corrected by padding silence / dropping samples. +const AUDIO_DRIFT_TOLERANCE: f64 = 0.15; +/// Decoded audio frames are ~10 ms or longer, so one second of buffered +/// audio carries well under this many timestamps. +const PTS_MARKERS_CAP: usize = 256; + +/// Media time anchor: ties a media timestamp to a position in the sample +/// stream. +#[derive(Clone, Copy)] +struct PtsMarker { + /// Stream position, in interleaved samples since the ring pair was + /// created. + position: u64, + /// Media time of the sample at `position`, in seconds. + pts: f64, +} + +/// What the audio callback must do after drift correction. +pub(super) enum ClockSync { + /// Audio is aligned with the master clock (late samples were already + /// dropped): consume normally. + Consume, + /// Audio is ahead of the master clock: emit silence to hold it back. + EmitSilence, +} + +/// Slot through which the receiver half is handed to the audio thread. +pub(super) type SharedAudioReceiver = Arc>>; + +/// The worker's grip on the whole ring: the sender half it fills, paired +/// with the slot its receiver half was published into. Both halves are +/// created and replaced together, so they always originate from the same +/// ring. +pub(super) struct AudioRing { + tx: AudioSender, + rx_slot: SharedAudioReceiver, +} + +impl AudioRing { + /// Creates a ring for `audio_options` and publishes its receiver into + /// `rx_slot`. `playback_rate` is the varispeed the buffered samples + /// were converted for; it scales the receiver's media-time math. + pub(super) fn new( + audio_options: AudioOptions, + playback_rate: f64, + rx_slot: SharedAudioReceiver, + ) -> Self { + let (tx, rx) = split(audio_options, playback_rate); + *rx_slot.lock() = Some(rx); + Self { tx, rx_slot } + } + + /// Replaces both halves with a fresh ring for `audio_options` at + /// `playback_rate`, discarding whatever the old ring still buffered. + pub(super) fn replace(&mut self, audio_options: AudioOptions, playback_rate: f64) { + let (tx, rx) = split(audio_options, playback_rate); + self.tx = tx; + *self.rx_slot.lock() = Some(rx); + } + + /// Appends a decoded frame when the ring has room. An empty ring always + /// accepts, so an oversized frame cannot deadlock the pipeline. + pub(super) fn try_extend(&mut self, pts: Option, samples: &[f32]) -> bool { + self.tx.try_extend(pts, samples) + } + + /// Whether the receiver has consumed everything pushed so far. + pub(super) fn is_drained(&self) -> bool { + self.tx.is_drained() + } +} + +fn split(audio_options: AudioOptions, playback_rate: f64) -> (AudioSender, AudioReceiver) { + let per_second = (audio_options.sample_rate.get() as usize) + .saturating_mul(audio_options.channels_usize().get()); + let capacity = (per_second as f64 * AUDIO_BUFFER_SECONDS) as usize; + + let (samples_tx, samples_rx) = HeapRb::new(capacity).split(); + let (markers_tx, markers_rx) = HeapRb::new(PTS_MARKERS_CAP).split(); + + ( + AudioSender { + samples: samples_tx, + markers: markers_tx, + written: 0, + }, + AudioReceiver { + samples: samples_rx, + markers: markers_rx, + read: 0, + anchor: None, + pending: None, + audio_options, + playback_rate, + }, + ) +} + +/// Worker-side half: pushes decoded samples and their timestamps. +struct AudioSender { + samples: HeapProd, + markers: HeapProd, + /// Interleaved samples pushed since creation; stream position of the + /// next pushed sample. + written: u64, +} + +impl AudioSender { + fn try_extend(&mut self, pts: Option, samples: &[f32]) -> bool { + if self.samples.vacant_len() < samples.len() && !self.samples.is_empty() { + return false; + } + if let Some(pts) = pts { + // a full marker ring only costs one re-anchor: harmless + let _ = self.markers.try_push(PtsMarker { + position: self.written, + pts, + }); + } + // when accepted into an empty ring, a frame larger than the whole + // ring is truncated (the VecDeque predecessor grew unboundedly); + // sane decoder output is orders of magnitude below the capacity + let pushed = self.samples.push_slice(samples); + self.written = self.written.saturating_add(pushed as u64); + true + } + + fn is_drained(&self) -> bool { + self.samples.is_empty() + } +} + +/// Audio-thread half: pops samples wait-free and tracks their media time. +pub(super) struct AudioReceiver { + samples: HeapCons, + markers: HeapCons, + /// Interleaved samples consumed since creation; stream position of the + /// sample at the ring head. + read: u64, + /// Newest marker at or before `read`; media time is extrapolated from + /// it, `None` until timed samples arrive (drift correction disabled). + anchor: Option, + /// A marker taken off the ring that still lies ahead of `read`. + pending: Option, + audio_options: AudioOptions, + /// Varispeed the samples were converted for: one output frame covers + /// `playback_rate / sample_rate` media seconds. Constant per ring — + /// a rate change replaces the whole ring. + playback_rate: f64, +} + +impl AudioReceiver { + pub(super) const fn channel_count(&self) -> NonZeroUsize { + self.audio_options.channels_usize() + } + + fn rate_f64(&self) -> f64 { + self.audio_options.sample_rate_f64() + } + + /// Media time of the sample at the ring head: the newest marker at or + /// before `read`, extrapolated by the frames consumed past it. + fn next_pts(&mut self) -> Option { + loop { + let marker = match self.pending.take() { + Some(marker) => marker, + None => match self.markers.try_pop() { + Some(marker) => marker, + None => break, + }, + }; + if marker.position <= self.read { + self.anchor = Some(marker); + } else { + self.pending = Some(marker); + break; + } + } + + self.anchor.map(|marker| { + let samples_past = (self.read.saturating_sub(marker.position)) as usize; + let frames_past = samples_past / self.channel_count(); + marker.pts + frames_past as f64 * self.playback_rate / self.rate_f64() + }) + } + + /// Applies master-clock drift correction. + pub(super) fn sync_to_clock(&mut self, now: f64) -> ClockSync { + let Some(pts) = self.next_pts() else { + return ClockSync::Consume; + }; + let drift = pts - now; + if drift > AUDIO_DRIFT_TOLERANCE { + return ClockSync::EmitSilence; + } + if drift < -AUDIO_DRIFT_TOLERANCE { + let channels_nz = self.channel_count(); + // a late media span occupies span / rate output frames + let late_frames = (-drift * self.rate_f64() / self.playback_rate) as usize; + let buffered_frames = self.samples.occupied_len() / channels_nz; + let drop_samples = late_frames + .min(buffered_frames) + .saturating_mul(channels_nz.get()); + let dropped = self.samples.skip(drop_samples); + self.read = self.read.saturating_add(dropped as u64); + } + ClockSync::Consume + } + + /// Copies whole frames into `out`, zero-fills the rest, and advances + /// the stream position; returns the number of frames copied. + pub(super) fn read_into(&mut self, out: &mut [f32]) -> usize { + let channels_nz = self.channel_count(); + let requested_frames = out.len() / channels_nz; + let buffered_frames = self.samples.occupied_len() / channels_nz; + let copied_samples = requested_frames + .min(buffered_frames) + .saturating_mul(channels_nz.get()); + + let copied = match out.get_mut(..copied_samples) { + Some(dst) => self.samples.pop_slice(dst), + None => 0, + }; + if let Some(rest) = out.get_mut(copied..) { + rest.fill(0.0); + } + + self.read = self.read.saturating_add(copied as u64); + copied / channels_nz + } +} diff --git a/Explorer/Assets/Plugins/UUAV/native/src/playback/clock.rs b/Explorer/Assets/Plugins/UUAV/native/src/playback/clock.rs new file mode 100644 index 00000000000..68ac42aa570 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/src/playback/clock.rs @@ -0,0 +1,69 @@ +use std::time::Instant; + +/// Master clock corrections below this are ignored to avoid jitter. +pub(super) const CLOCK_SNAP_THRESHOLD: f64 = 0.05; + +/// Media timeline clock. Free-runs on the monotonic clock while playing and +/// snaps to the externally assigned master clock when it drifts. +/// +/// An immutable snapshot published through `ArcSwap`: updates replace the +/// whole value, readers (render/audio threads) never block. +#[derive(Clone, Copy)] +pub(super) struct Clock { + base: f64, + anchor: Option, + /// Media seconds advanced per wall-clock second while running. + rate: f64, +} + +impl Clock { + pub(super) const fn new() -> Self { + Self { + base: 0.0, + anchor: None, + rate: 1.0, + } + } + + pub(super) fn now(&self) -> f64 { + self.base + + self + .anchor + .map_or(0.0, |a| a.elapsed().as_secs_f64() * self.rate) + } + + /// Re-anchored at `time`, preserving the running/held state. + pub(super) fn at(self, time: f64) -> Self { + Self { + base: time, + anchor: self.anchor.map(|_| Instant::now()), + rate: self.rate, + } + } + + pub(super) fn running(self) -> Self { + Self { + base: self.base, + anchor: self.anchor.or_else(|| Some(Instant::now())), + rate: self.rate, + } + } + + pub(super) fn held(self) -> Self { + Self { + base: self.now(), + anchor: None, + rate: self.rate, + } + } + + /// Runs at `rate` from here on: re-anchored at the current time, so + /// the already-elapsed span keeps the scale it was played at. + pub(super) fn with_rate(self, rate: f64) -> Self { + Self { + base: self.now(), + anchor: self.anchor.map(|_| Instant::now()), + rate, + } + } +} diff --git a/Explorer/Assets/Plugins/UUAV/native/src/playback/control.rs b/Explorer/Assets/Plugins/UUAV/native/src/playback/control.rs new file mode 100644 index 00000000000..77398b5d506 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/src/playback/control.rs @@ -0,0 +1,167 @@ +//! Single-slot control channel: the push control overwrites the latest value, the +//! consume control either takes the pending update once or peeks the +//! current value at any time - the value survives consumption. + +use parking_lot::Mutex; +use std::sync::{Arc, Weak}; + +/// The value and its update flag, mutated together in one critical +/// section: a consumer can never observe a half-pushed pair, and each +/// push is delivered exactly once. +struct State { + /// Latest pushed value; survives consumption. + value: T, + /// A push not consumed yet. + pending: bool, +} + +type Slot = Mutex>; + +pub(crate) struct ControlPush(Arc>); + +impl ControlPush { + pub(crate) fn new(initial: T) -> Self { + Self(Arc::new(Mutex::new(State { + value: initial, + pending: false, + }))) + } + + /// Overwrites the value and marks it pending, as one atomic pair; + /// the latest push wins. + pub(crate) fn push(&self, value: T) { + let mut state = self.0.lock(); + state.value = value; + state.pending = true; + } + + /// Current value, pending or not. + pub(crate) fn latest(&self) -> T { + self.0.lock().value.clone() + } + + /// Current value plus whether the latest push is still awaiting + /// consumption, as one atomic pair. + pub(crate) fn snapshot(&self) -> (T, bool) { + let state = self.0.lock(); + (state.value.clone(), state.pending) + } + + /// A consumer half on the same slot. Weak, so a consumer neither + /// keeps a freed player's control state alive nor obeys it. + pub(crate) fn consumer(&self) -> ControlConsume { + ControlConsume(Arc::downgrade(&self.0)) + } +} + +pub(crate) struct ControlConsume(Weak>); + +impl Clone for ControlConsume { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl ControlConsume { + /// Takes the pending update: the current value once per push, `None` + /// while nothing new was pushed (the value itself persists — see + /// [`Self::peek`]) or when the push half is gone. + pub(crate) fn consume(&self) -> Option { + let slot = self.0.upgrade()?; + let mut state = slot.lock(); + if state.pending { + state.pending = false; + Some(state.value.clone()) + } else { + None + } + } + + /// Current value regardless of pending; `None` when the push half is + /// gone. + pub(crate) fn peek(&self) -> Option { + Some(self.0.upgrade()?.lock().value.clone()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use anyhow::{Context, Result}; + + #[test] + fn consume_takes_once_but_value_persists() -> Result<()> { + let push = ControlPush::new(1); + let consume = push.consumer(); + + assert_eq!(consume.consume(), None, "nothing pushed yet"); + assert_eq!(consume.peek().context("push half alive")?, 1); + + push.push(2); + assert_eq!(consume.consume().context("pending update")?, 2); + assert_eq!(consume.consume(), None, "already consumed"); + assert_eq!(consume.peek().context("value persists")?, 2); + Ok(()) + } + + #[test] + fn each_push_is_consumed_exactly_once() -> Result<()> { + let push = ControlPush::new(0); + let consume = push.consumer(); + + push.push(1); + assert_eq!(consume.consume().context("first update")?, 1); + push.push(2); + assert_eq!(consume.consume().context("second update")?, 2); + assert_eq!(consume.consume(), None, "no duplicate delivery"); + Ok(()) + } + + #[test] + fn latest_push_wins() -> Result<()> { + let push = ControlPush::new(0); + let consume = push.consumer(); + + push.push(1); + push.push(2); + assert_eq!(consume.consume().context("pending update")?, 2); + assert_eq!(push.latest(), 2); + Ok(()) + } + + #[test] + fn put_back_hands_the_command_to_the_next_consumer() -> Result<()> { + let push = ControlPush::new(0); + let zombie = push.consumer(); + + push.push(7); + assert_eq!(zombie.consume().context("pending update")?, 7); + Ok(()) + } + + #[test] + fn snapshot_reports_pending_until_consumed() -> Result<()> { + let push = ControlPush::new(1); + let consume = push.consumer(); + + assert_eq!(push.snapshot(), (1, false), "nothing pushed yet"); + + push.push(2); + assert_eq!(push.snapshot(), (2, true), "push awaits consumption"); + + consume.consume().context("pending update")?; + assert_eq!(push.snapshot(), (2, false), "consumed, value persists"); + Ok(()) + } + + #[test] + fn dead_push_half_reads_none() { + let push = ControlPush::new(1); + let consume = push.consumer(); + push.push(2); + drop(push); + + assert_eq!(consume.consume(), None); + assert_eq!(consume.peek(), None); + } +} diff --git a/Explorer/Assets/Plugins/UUAV/native/src/playback/input.rs b/Explorer/Assets/Plugins/UUAV/native/src/playback/input.rs new file mode 100644 index 00000000000..1a6b58ace1b --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/src/playback/input.rs @@ -0,0 +1,156 @@ +use anyhow::{Result, anyhow}; +use ffmpeg_sys_next as ff; +use std::ffi::CString; +use std::os::raw::{c_int, c_void}; +use std::ptr; +use std::sync::atomic::{AtomicBool, Ordering}; + +use super::util::ReadOnlyCancelToken; +use crate::ffutil::{AvDict, Stream, StreamingProtocol, check}; + +/// Aborts blocking demuxer I/O when the playback is being closed. +unsafe extern "C" fn interrupt_cb(opaque: *mut c_void) -> c_int { + if opaque.is_null() { + return 0; + } + let cancelled = unsafe { &*opaque.cast::() }; + c_int::from(cancelled.load(Ordering::Relaxed)) +} + +/// Owning wrapper around the demuxer of one opened url. +pub(super) struct Input { + fmt: *mut ff::AVFormatContext, + /// Never read: keeps the flag the demuxer's interrupt callback points + /// at alive for as long as the context holds the raw pointer. + _cancel: ReadOnlyCancelToken, +} + +impl Input { + /// Opens the media and probes its streams. Demuxer I/O is aborted + /// through `cancel` (see [`interrupt_cb`]); `protocol_whitelist` bounds the + /// protocols the demuxer and any nested (HLS segment) contexts may use. + pub(super) fn open( + cancel_token: ReadOnlyCancelToken, + url: &str, + protocol_whitelist: &StreamingProtocol, + ) -> Result { + let url_c = CString::new(url).map_err(|_| anyhow!("url contains a NUL byte"))?; + let mut opts = AvDict::from(protocol_whitelist); + + let input = unsafe { + let mut fmt = ff::avformat_alloc_context(); + if fmt.is_null() { + return Err(anyhow!("avformat_alloc_context failed")); + } + (*fmt).interrupt_callback = ff::AVIOInterruptCB { + callback: Some(interrupt_cb), + opaque: cancel_token.as_flag_ptr().cast_mut().cast::(), + }; + + // avformat_open_input frees the context on failure + check( + "avformat_open_input", + ff::avformat_open_input(&mut fmt, url_c.as_ptr(), ptr::null(), opts.as_mut_ptr()), + )?; + + Self { + fmt, + _cancel: cancel_token, + } + }; + + check("avformat_find_stream_info", unsafe { + ff::avformat_find_stream_info(input.fmt, ptr::null_mut()) + })?; + Ok(input) + } + + pub(super) const fn as_ptr(&self) -> *mut ff::AVFormatContext { + self.fmt + } + + pub(super) fn find_best_stream(&self, media_type: ff::AVMediaType) -> c_int { + unsafe { ff::av_find_best_stream(self.fmt, media_type, -1, -1, ptr::null_mut(), 0) } + } + + /// SAFETY: the streams belong to the context, which outlives the view + pub(super) fn stream_at(&self, index: c_int) -> Stream { + unsafe { Stream::from_raw(*(*self.fmt).streams.offset(index as isize)) } + } + + /// Timestamp of the first frame in seconds; the media timeline is + /// normalized so playback starts at 0. + pub(super) fn start_offset(&self) -> f64 { + let start_time = unsafe { (*self.fmt).start_time }; + if start_time == ff::AV_NOPTS_VALUE { + 0.0 + } else { + start_time as f64 / f64::from(ff::AV_TIME_BASE) + } + } + + /// Total duration in seconds, `None` for realtime streams. + pub(super) fn duration(&self) -> Option { + let duration = unsafe { (*self.fmt).duration }; + if duration == ff::AV_NOPTS_VALUE || duration <= 0 { + None + } else { + Some(duration as f64 / f64::from(ff::AV_TIME_BASE)) + } + } +} + +impl Drop for Input { + fn drop(&mut self) { + unsafe { ff::avformat_close_input(&mut self.fmt) }; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::playback::util::CancelToken; + + fn cancel() -> ReadOnlyCancelToken { + CancelToken::new().into() + } + + // A path that does not exist, so `file:` never reads anything real: the + // point is only whether FFmpeg is *allowed* to reach the filesystem. + const FILE_URL: &str = "file:///nonexistent/uuav-protocol-whitelist-test.mp4"; + + // `Input` is not `Debug` + fn open_error(whitelist: &str) -> Result { + let whitelist = CString::new(whitelist)?; + // SAFETY: `whitelist` is a valid NUL-terminated C string. + let protocol = unsafe { StreamingProtocol::new(whitelist.as_ptr()) }?; + match Input::open(cancel(), FILE_URL, &protocol) { + Ok(_) => Err(anyhow!("opening a nonexistent file must fail")), + Err(e) => Ok(e.to_string().to_lowercase()), + } + } + + #[test] + fn file_allowed_when_whitelisted_reaches_filesystem() -> Result<()> { + // Protocol allowed → the failure is the missing file, meaning FFmpeg + // got past the whitelist gate and hit the filesystem. + let msg = open_error("file")?; + assert!( + msg.contains("no such file"), + "expected a filesystem error, got: {msg}" + ); + Ok(()) + } + + #[test] + fn file_denied_before_touching_filesystem() -> Result<()> { + // Blocked at the gate → FFmpeg never reached the filesystem, so this + // is NOT a "no such file" error. + let msg = open_error("https,tls,tcp")?; + assert!( + !msg.contains("no such file"), + "file: must be blocked before the filesystem, got: {msg}" + ); + Ok(()) + } +} diff --git a/Explorer/Assets/Plugins/UUAV/native/src/playback/mod.rs b/Explorer/Assets/Plugins/UUAV/native/src/playback/mod.rs new file mode 100644 index 00000000000..7e01f2b0f00 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/src/playback/mod.rs @@ -0,0 +1,35 @@ +//! Playback of a single url: the blocking open/run loop the player drives +//! on a dedicated thread, and the resources the engine-facing threads +//! consume. +//! +//! Everything here is internal to the module except what the player +//! orchestrates: [`PlaybackUnit`] with its demux/decode pipeline, the +//! [`CancelToken`] that stops it, and the [`fill_silence`]/[`report`] +//! helpers. + +mod audio_playback; +mod audio_ring; +mod clock; +mod control; +mod input; +mod transport; +mod unit; +mod util; +mod video_playback; + +pub(crate) use control::ControlPush; +pub(crate) use unit::{DEFAULT_PLAYBACK_RATE, PlaybackUnit, UnitControls}; +pub(crate) use util::CancelToken; +pub(crate) use util::ReadOnlyCancelToken; + +use std::num::NonZeroUsize; + +/// Zero-fills an engine-provided interleaved sample buffer. +pub(crate) fn fill_silence(dst: *mut f32, frames: usize, channels: NonZeroUsize) { + let Some(total) = frames.checked_mul(channels.get()) else { + return; + }; + // the engine guarantees dst holds nb_frames * channels floats + let out = unsafe { std::slice::from_raw_parts_mut(dst, total) }; + out.fill(0.0); +} diff --git a/Explorer/Assets/Plugins/UUAV/native/src/playback/transport.rs b/Explorer/Assets/Plugins/UUAV/native/src/playback/transport.rs new file mode 100644 index 00000000000..9a1f67e3392 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/src/playback/transport.rs @@ -0,0 +1,127 @@ +//! Transport state machine: the playback state and the media clock it +//! drives, published as one atomic snapshot. + +use arc_swap::ArcSwap; + +use super::clock::{CLOCK_SNAP_THRESHOLD, Clock}; +use crate::UUAVState; + +/// State of a live playback. Mirrors [`UUAVState`] minus the states a +/// playback unit cannot be in: CLOSED, OPENING and ERROR (encoded by the +/// player as the absence of a unit — a unit that exists is open and +/// healthy) and UNKNOWN (fabricated by the FFI layer for missing players). +#[derive(Clone, Copy)] +pub(super) enum PlaybackState { + Ready, + Playing, + Paused, + Ended, +} + +impl From for UUAVState { + fn from(state: PlaybackState) -> Self { + match state { + PlaybackState::Ready => Self::UUAV_READY, + PlaybackState::Playing => Self::UUAV_PLAYING, + PlaybackState::Paused => Self::UUAV_PAUSED, + PlaybackState::Ended => Self::UUAV_ENDED, + } + } +} + +/// State and clock as one value: a transition replaces both at once, so +/// readers can never observe them disagreeing (e.g. ENDED with a running +/// clock). +struct Snapshot { + state: PlaybackState, + clock: Clock, +} + +/// Transport shared between the engine-facing threads (control, render, +/// audio) and the playback thread, atomic across all of them: transitions +/// swap the whole snapshot; readers never block. +pub(super) struct AtomicTransport(ArcSwap); + +impl AtomicTransport { + pub(super) fn new() -> Self { + Self(ArcSwap::from_pointee(Snapshot { + state: PlaybackState::Ready, + clock: Clock::new(), + })) + } + + pub(super) fn state(&self) -> PlaybackState { + self.0.load().state + } + + /// Media time at the clock, in seconds. + pub(super) fn now(&self) -> f64 { + self.0.load().clock.now() + } + + pub(super) fn is_playing(&self) -> bool { + matches!(self.state(), PlaybackState::Playing) + } + + /// Starts the clock and flags PLAYING. + pub(super) fn play(&self) { + self.0.rcu(|s| Snapshot { + state: PlaybackState::Playing, + clock: s.clock.running(), + }); + } + + /// Holds the clock and flags PAUSED. + pub(super) fn pause(&self) { + match self.state() { + PlaybackState::Playing => { + self.0.rcu(|s| Snapshot { + state: PlaybackState::Paused, + clock: s.clock.held(), + }); + } + PlaybackState::Ready | PlaybackState::Paused | PlaybackState::Ended => {} + } + } + + /// Fully consumed: holds the clock and flags ENDED. + pub(super) fn ended(&self) { + self.0.rcu(|s| Snapshot { + state: PlaybackState::Ended, + clock: s.clock.held(), + }); + } + + /// Changes the clock rate in place, preserving the state and the + /// current media time. + pub(super) fn set_rate(&self, rate: f64) { + self.0.rcu(|s| Snapshot { + state: s.state, + clock: s.clock.with_rate(rate), + }); + } + + /// A seek was applied: re-anchors the clock at `target`. A seek out of + /// ENDED lands paused; `play` (which queues the restart seek itself) + /// flips the state to PLAYING on its own. + pub(super) fn rebase(&self, target: f64) { + self.0.rcu(|s| Snapshot { + state: match s.state { + PlaybackState::Ended => PlaybackState::Paused, + state => state, + }, + clock: s.clock.at(target), + }); + } + + /// Slaves the clock to the externally provided master clock; + /// corrections below the snap threshold are ignored to avoid jitter. + pub(super) fn sync_to_master(&self, current_time: f64) { + if (self.now() - current_time).abs() > CLOCK_SNAP_THRESHOLD { + self.0.rcu(|s| Snapshot { + state: s.state, + clock: s.clock.at(current_time), + }); + } + } +} diff --git a/Explorer/Assets/Plugins/UUAV/native/src/playback/unit.rs b/Explorer/Assets/Plugins/UUAV/native/src/playback/unit.rs new file mode 100644 index 00000000000..35aa4c8885e --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/src/playback/unit.rs @@ -0,0 +1,523 @@ +use anyhow::{Result, anyhow}; +use ffmpeg_sys_next as ff; +use parking_lot::Mutex; +use std::sync::Once; +use std::thread; + +use super::audio_playback::{AudioPlayback, AudioReader}; +use super::control::ControlConsume; +use super::input::Input; +use super::transport::{AtomicTransport, PlaybackState}; +use super::util::{AtomicSeekSlot, PLAYBACK_POLL, ReadOnlyCancelToken}; +use super::video_playback::{VideoPlayback, VideoQueue, VideoReader}; +use crate::ffutil::{OwnedPacket, Stream, StreamingProtocol, av_err, check, copy_c_name, q2d}; +use crate::hw_device::{HwDevice, HwDeviceContext}; +use crate::video_output::{VideoOutput, VideoTextureView}; +use crate::{AudioOptionsView, ErrorCallback, MediaInfo, UUAVState, VideoSize}; +use std::mem; +use std::os::raw::c_int; + +static NETWORK_INIT: Once = Once::new(); + +/// A video frame is presented once the clock is within this of its pts. +const VIDEO_PRESENT_BIAS: f64 = 0.005; + +pub const DEFAULT_PLAYBACK_RATE: f64 = 1.0; + +/// The demux/decode half of a playback: everything [`PlaybackUnit::run_blocking`] +/// needs mutable and exclusive on the playback thread. +pub(crate) struct Pipeline { + input: Input, + video: Option, + audio: Option, + /// Timestamp of the first frame; the media timeline is normalized so + /// playback starts at 0. + start_offset: f64, +} + +pub(crate) struct UnitControls { + /// `true` = a play queued while no unit was live, `false` = a pause + pub(crate) play_or_pause: ControlConsume, + /// Wrap at EOF on the playback thread instead of settling into ENDED. + pub(crate) looping: ControlConsume, + /// Desired playback rate, in media seconds per wall second. + pub(crate) rate: ControlConsume, +} + +/// Playback of a single url: the shared state the engine-facing threads +/// (control, render, audio) and the playback thread work against. +/// +/// A unit that exists is fully open — [`PlaybackUnit::open`] builds it +/// from an already-opened media, so every field here is plain data. +pub(crate) struct PlaybackUnit { + url: String, + cancel: ReadOnlyCancelToken, + /// Coalescing seek command; the playback thread services it. + seek: AtomicSeekSlot, + controls: UnitControls, + /// Playback state and media clock, as one atomic snapshot. + transport: AtomicTransport, + /// Audio-thread half of the playback's audio; the pipeline's + /// [`AudioPlayback`] feeds it. + audio: AudioReader, + /// Render-thread half of the presentation queue; the pipeline's + /// [`VideoPlayback`] feeds it. + video: VideoReader, + /// Windows-Only D3D11VA device context frames decode through; `None` for + /// audio-only media. + #[cfg(target_os = "windows")] + hw_ctx: Option, + /// Total duration in seconds, `None` for realtime streams. + duration: Option, + video_size: Option, + /// Source stream parameters, captured while probing in [`Self::open`]. + media_info: MediaInfo, + /// Presentation target; created lazily on the render thread. + output: Mutex>, + /// Engine device the presentation output is created on. + device: HwDevice, + error_callback: ErrorCallback, +} + +/// Fills the video half of `info` from the probed stream's parameters. +fn probe_video_info(info: &mut MediaInfo, stream: Stream) { + let par = stream.codecpar(); + info.has_video = 1; + copy_c_name(&mut info.video_codec, unsafe { + ff::avcodec_get_name(stream.codec_id()) + }); + // SAFETY: the value was probed by FFmpeg itself, so it is a valid + // AVPixelFormat (or -1 = NONE, for which the name lookup returns null) + let pix_fmt = unsafe { mem::transmute::((*par).format) }; + copy_c_name(&mut info.pixel_format, unsafe { + ff::av_get_pix_fmt_name(pix_fmt) + }); + unsafe { + info.width = u32::try_from((*par).width).unwrap_or(0); + info.height = u32::try_from((*par).height).unwrap_or(0); + info.video_bitrate = (*par).bit_rate.max(0); + } + info.framerate = q2d(stream.avg_frame_rate()); +} + +/// Fills the audio half of `info` from the probed stream's parameters. +fn probe_audio_info(info: &mut MediaInfo, stream: Stream) { + let par = stream.codecpar(); + info.has_audio = 1; + copy_c_name(&mut info.audio_codec, unsafe { + ff::avcodec_get_name(stream.codec_id()) + }); + // SAFETY: the value was probed by FFmpeg itself, so it is a valid + // AVSampleFormat (or -1 = NONE, for which the name lookup returns null) + let sample_fmt = unsafe { mem::transmute::((*par).format) }; + copy_c_name(&mut info.sample_format, unsafe { + ff::av_get_sample_fmt_name(sample_fmt) + }); + unsafe { + info.sample_rate = (*par).sample_rate.max(0); + info.channels = (*par).ch_layout.nb_channels.max(0); + info.audio_bitrate = (*par).bit_rate.max(0); + } +} + +impl PlaybackUnit { + /// [playback thread] Opens the media at `url`, blocking until its + /// streams are probed and the decoders are built. Aborted through + /// `cancel` while blocked on demuxer I/O. + pub(crate) fn open( + url: String, + device: HwDevice, + audio_out: AudioOptionsView, + cancel: ReadOnlyCancelToken, + error_callback: ErrorCallback, + protocol_whitelist: &StreamingProtocol, + controls: UnitControls, + ) -> Result<(Self, Pipeline)> { + NETWORK_INIT.call_once(|| { + unsafe { ff::avformat_network_init() }; + }); + + let input = Input::open(cancel.clone(), &url, protocol_whitelist)?; + let video_index = input.find_best_stream(ff::AVMediaType::AVMEDIA_TYPE_VIDEO); + let audio_index = input.find_best_stream(ff::AVMediaType::AVMEDIA_TYPE_AUDIO); + if video_index < 0 && audio_index < 0 { + return Err(anyhow!("media has no playable video or audio stream")); + } + + let media_info = { + let mut media_info = MediaInfo::empty(); + if let Some(d) = input.duration() { + media_info.duration = d; + } + if video_index >= 0 { + probe_video_info(&mut media_info, input.stream_at(video_index)); + } + if audio_index >= 0 { + probe_audio_info(&mut media_info, input.stream_at(audio_index)); + } + media_info + }; + + let audio_reader = AudioReader::new(audio_out.clone()); + let (video_queue, video_reader) = VideoQueue::channel(); + + let (hw_ctx, video, video_size) = if video_index >= 0 { + #[cfg(target_os = "windows")] + let hw = HwDeviceContext::new(&device)?; + #[cfg(target_os = "macos")] + let hw = HwDeviceContext::new()?; + + let playback = + VideoPlayback::new(input.stream_at(video_index), video_index, &hw, video_queue)?; + let par = input.stream_at(video_index).codecpar(); + let (width, height) = unsafe { ((*par).width, (*par).height) }; + let size = VideoSize { + width: u32::try_from(width).map_err(|_| anyhow!("invalid video width: {width}"))?, + height: u32::try_from(height) + .map_err(|_| anyhow!("invalid video height: {height}"))?, + }; + (Some(hw), Some(playback), Some(size)) + } else { + (None, None, None) + }; + + let audio = if audio_index >= 0 { + Some(AudioPlayback::new( + input.stream_at(audio_index), + audio_index, + audio_out, + audio_reader.rx_slot(), + )?) + } else { + None + }; + + #[cfg(target_os = "macos")] + drop(hw_ctx); + + let unit = Self { + duration: input.duration(), + video_size, + media_info, + cancel, + seek: AtomicSeekSlot::new(), + controls, + transport: AtomicTransport::new(), + audio: audio_reader, + video: video_reader, + #[cfg(target_os = "windows")] + hw_ctx, + output: Mutex::new(None), + device, + error_callback, + url, + }; + let pipeline = Pipeline { + start_offset: input.start_offset(), + input, + video, + audio, + }; + Ok((unit, pipeline)) + } + + /// [playback thread] Demuxes and decodes until the player cancels. + /// A cancelled run is a clean exit; a runtime error propagates to the + /// player, which retires this unit. + pub(crate) fn run_blocking(&self, pipeline: Pipeline) -> Result<()> { + match self.run(pipeline) { + Err(e) if !self.cancel.is_cancelled() => Err(e), + Err(_) | Ok(()) => Ok(()), + } + } + + fn run(&self, pipeline: Pipeline) -> Result<()> { + let Pipeline { + input, + mut video, + mut audio, + start_offset, + } = pipeline; + + let mut packet = OwnedPacket::new()?; + // whether the demuxer ran out of input; playback then drains the sinks + let mut eof = false; + // varispeed this unit currently runs at; fresh units start at 1x + let mut applied_rate = 1.0_f64; + // the sinks poll this while waiting for queue space: not playing + // means nothing drains, so a blocked pump is the only place a + // queued play can be noticed + let poll_controls = || self.apply_play_or_pause(); + + loop { + if self.cancel.is_cancelled() { + return Ok(()); + } + + self.apply_play_or_pause(); + + // peek, not consume: a fresh unit picks up the standing rate + // even though a previous unit already took the update. + // realtime streams (no duration) stay at 1x + let rate = if self.duration.is_some() { + self.controls.rate.peek().unwrap_or(DEFAULT_PLAYBACK_RATE) + } else { + DEFAULT_PLAYBACK_RATE + }; + if rate.to_bits() != applied_rate.to_bits() { + self.transport.set_rate(rate); + if let Some(audio) = audio.as_mut() { + audio.set_rate(rate); + } + applied_rate = rate; + } + + if let Some(target) = self.seek.take() { + // non-fatal: an unseekable (e.g. realtime) stream keeps playing + if let Err(e) = self.apply_seek( + &input, + video.as_mut(), + audio.as_mut(), + &mut eof, + target, + start_offset, + ) { + self.error_callback + .report(format!("seek to {target}s failed: {e}")); + } + continue; + } + + if eof { + if self.loop_wrap( + &input, + video.as_mut(), + audio.as_mut(), + &mut eof, + start_offset, + ) { + continue; + } + self.settle_ended(video.as_ref(), audio.as_ref()); + thread::sleep(PLAYBACK_POLL); + continue; + } + + let ret = unsafe { ff::av_read_frame(input.as_ptr(), packet.as_mut_ptr()) }; + if ret == ff::AVERROR_EOF { + if let Some(video) = video.as_mut() { + video.drain(start_offset, &self.cancel, &self.seek, &poll_controls)?; + } + if let Some(audio) = audio.as_mut() { + audio.drain(start_offset, &self.cancel, &self.seek, &poll_controls)?; + } + eof = true; + continue; + } + if ret == ff::AVERROR(libc::EAGAIN) { + thread::sleep(PLAYBACK_POLL); + continue; + } + if ret < 0 { + return Err(av_err("av_read_frame", ret)); + } + + let stream_index = packet.stream_index(); + if let Some(video) = video.as_mut() + && video.handles(stream_index) + { + video.handle_packet( + &mut packet, + start_offset, + &self.cancel, + &self.seek, + &poll_controls, + )?; + } else if let Some(audio) = audio.as_mut() + && audio.handles(stream_index) + { + audio.handle_packet( + &mut packet, + start_offset, + &self.cancel, + &self.seek, + &poll_controls, + )?; + } + packet.unref(); + } + } + + /// [playback thread] At EOF with looping on: once the sinks have + /// played the tail out, wraps back to the start right here — the + /// input and the decoders stay warm, and no ENDED/seek round-trip is + /// exposed to the engine. Returns whether the wrap happened. + fn loop_wrap( + &self, + input: &Input, + video: Option<&mut VideoPlayback>, + audio: Option<&mut AudioPlayback>, + eof: &mut bool, + start_offset: f64, + ) -> bool { + if !self.controls.looping.peek().unwrap_or(false) || !self.transport.is_playing() { + return false; + } + + let drained = video.as_deref().is_none_or(VideoPlayback::is_drained) + && audio.as_deref().is_none_or(AudioPlayback::is_drained); + if !drained { + return false; + } + + // non-fatal, like a user seek; falling through settles into ENDED, + // where is_playing turns false, so a failed wrap does not retry + if let Err(e) = self.apply_seek(input, video, audio, eof, 0.0, start_offset) { + self.error_callback + .report(format!("loop restart failed: {e}")); + return false; + } + true + } + + /// Fully decoded; once the sinks have drained, holds the clock and + /// flags ENDED. + fn settle_ended(&self, video: Option<&VideoPlayback>, audio: Option<&AudioPlayback>) { + let drained = video.is_none_or(VideoPlayback::is_drained) + && audio.is_none_or(AudioPlayback::is_drained); + if drained && self.transport.is_playing() { + self.transport.ended(); + } + } + + fn apply_seek( + &self, + input: &Input, + video: Option<&mut VideoPlayback>, + audio: Option<&mut AudioPlayback>, + eof: &mut bool, + target: f64, + start_offset: f64, + ) -> Result<()> { + let ts = ((target + start_offset) * f64::from(ff::AV_TIME_BASE)) as i64; + check("avformat_seek_file", unsafe { + ff::avformat_seek_file(input.as_ptr(), -1, i64::MIN, ts, i64::MAX, 0) + })?; + + if let Some(video) = video { + video.flush_for_seek(); + } + if let Some(audio) = audio { + audio.flush_for_seek(); + } + self.transport.rebase(target); + *eof = false; + Ok(()) + } + + pub(crate) fn state(&self) -> UUAVState { + self.transport.state().into() + } + + /// Applies a queued play/pause command, if any. + fn apply_play_or_pause(&self) { + if let Some(play) = self.controls.play_or_pause.consume() { + if play { + self.play(); + } else { + self.transport.pause(); + } + } + } + + fn play(&self) { + match self.transport.state() { + PlaybackState::Ready | PlaybackState::Paused => {} + PlaybackState::Playing => return, + PlaybackState::Ended => { + // restart from the beginning + self.seek.request(0.0); + } + } + self.transport.play(); + } + + pub(crate) fn seek_intent(&self, time: f64) { + self.seek.request(time.max(0.0)); + } + + pub(crate) const fn duration(&self) -> Option { + self.duration + } + + pub(crate) fn current_time(&self) -> f64 { + let now = self.transport.now(); + self.duration.map_or(now, |d| now.clamp(0.0, d)) + } + + /// The playback slaves itself to the externally provided master clock. + pub(crate) fn assign_master_clock(&self, current_time: f64) { + self.transport.sync_to_master(current_time); + } + + pub(crate) fn video_size(&self) -> Option { + self.video_size.clone() + } + + pub(crate) const fn media_info(&self) -> MediaInfo { + self.media_info + } + + pub(crate) fn video_texture( + &self, + #[cfg(target_os = "macos")] plane: i32, + ) -> Option { + #[cfg(target_os = "windows")] + let texture = self.output.lock().as_ref().and_then(VideoOutput::texture); + #[cfg(target_os = "macos")] + let texture = self + .output + .lock() + .as_ref() + .and_then(|output| output.texture(plane)); + + texture + } + + /// [render thread] Presents the frame that is due per the clock. + pub(crate) fn on_render_event(&self) { + let now = self.transport.now() + VIDEO_PRESENT_BIAS; + + // audio-only media never queues frames, so this also gates on video + let Some(frame) = self.video.next_due(now) else { + return; + }; + + let mut output = self.output.lock(); + let out = output.get_or_insert_with(|| VideoOutput::new(&self.device)); + + // D3D11 copies through the decode context (the immediate context + // under the FFmpeg device lock); Metal blits on the plugin's own + // queue and takes nothing from the decode context + #[cfg(target_os = "windows")] + let presented = match self.hw_ctx.as_ref() { + Some(hw) => out.present(hw, &frame), + None => return, + }; + #[cfg(target_os = "macos")] + let presented = out.present(&frame); + + if let Err(e) = presented { + let message = format!("video present failed for {}: {e}", self.url); + // Exit the guard scope before calling external FFI function + drop(output); + self.error_callback.report(message); + } + } + + /// [audio thread] Fills interleaved FLT; pads silence on underrun, + /// never blocks; returns frames actually copied. + pub(crate) fn read_audio(&self, dst: *mut f32, frames: usize) -> i32 { + self.audio.read(&self.transport, dst, frames) + } +} diff --git a/Explorer/Assets/Plugins/UUAV/native/src/playback/util.rs b/Explorer/Assets/Plugins/UUAV/native/src/playback/util.rs new file mode 100644 index 00000000000..f4b838bf504 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/src/playback/util.rs @@ -0,0 +1,83 @@ +//! Commands flowing from the engine-facing threads to the playback +//! thread: shutdown and seek. + +use arc_swap::ArcSwapOption; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Duration; + +/// Poll interval of the playback thread when it has to wait for queue +/// space or the next command. +pub(super) const PLAYBACK_POLL: Duration = Duration::from_millis(4); + +/// Shutdown command: makes the playback thread exit and aborts blocking +/// demuxer I/O. Cloning shares the token. +#[derive(Clone)] +pub(crate) struct CancelToken(Arc); + +impl CancelToken { + pub(crate) fn new() -> Self { + Self(Arc::new(AtomicBool::new(false))) + } + + pub(crate) fn cancel(&self) { + self.0.store(true, Ordering::Release); + } + + pub(crate) fn is_cancelled(&self) -> bool { + self.0.load(Ordering::Acquire) + } + + /// Address of the flag for the demuxer's C interrupt callback; valid + /// for as long as any clone of the token is alive. + fn as_flag_ptr(&self) -> *const AtomicBool { + Arc::as_ptr(&self.0) + } +} + +#[derive(Clone)] +pub(crate) struct ReadOnlyCancelToken(CancelToken); + +impl ReadOnlyCancelToken { + pub(crate) fn is_cancelled(&self) -> bool { + self.0.is_cancelled() + } + + /// Address of the flag for the demuxer's C interrupt callback; valid + /// for as long as any clone of the token is alive. + pub(crate) fn as_flag_ptr(&self) -> *const AtomicBool { + self.0.as_flag_ptr() + } +} + +impl From for ReadOnlyCancelToken { + fn from(c: CancelToken) -> Self { + Self(c) + } +} + +/// Coalescing seek command, atomic across threads: engine-facing threads +/// overwrite the pending target, the playback thread takes it. +pub(super) struct AtomicSeekSlot(ArcSwapOption); + +impl AtomicSeekSlot { + pub(super) fn new() -> Self { + Self(ArcSwapOption::empty()) + } + + /// Requests a seek; an unserviced previous request is simply + /// overwritten. + pub(super) fn request(&self, time: f64) { + self.0.store(Some(Arc::new(time))); + } + + /// [worker] Takes the pending request, leaving the slot empty. + pub(super) fn take(&self) -> Option { + self.0.swap(None).map(|target| *target) + } + + /// Whether a request is waiting to be serviced. + pub(super) fn is_pending(&self) -> bool { + self.0.load().is_some() + } +} diff --git a/Explorer/Assets/Plugins/UUAV/native/src/playback/video_playback.rs b/Explorer/Assets/Plugins/UUAV/native/src/playback/video_playback.rs new file mode 100644 index 00000000000..80d31213e15 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/src/playback/video_playback.rs @@ -0,0 +1,199 @@ +use anyhow::Result; +use crossbeam_channel::{Receiver, Sender, TrySendError, bounded}; +use parking_lot::Mutex; +use std::os::raw::c_int; +use std::ptr; +use std::sync::Arc; +use std::thread; + +use super::util::{AtomicSeekSlot, PLAYBACK_POLL, ReadOnlyCancelToken}; +use crate::ffutil::{Decoded, OwnedPacket, Stream}; +use crate::hw_device::HwDeviceContext; +use crate::video_decoder::{VideoDecoder, VideoFrame}; + +/// Decoded frames buffered ahead of the clock. Each entry pins a decoder +/// surface, so it must stay below [`VideoDecoder::EXTRA_HW_FRAMES`]. +const VIDEO_QUEUE_CAP: usize = 4; + +/// The frame the render thread popped but whose time has not come yet. +type HeldFrame = Arc>>; + +/// Worker half of the presentation queue: the sender it fills, plus a +/// second receiver and the reader's held-frame slot so a seek can discard +/// everything buffered. +#[derive(Clone)] +pub(super) struct VideoQueue { + tx: Sender, + /// Drains the channel on seek; frames dropped here release their + /// decoder surfaces immediately. + drain: Receiver, + held: HeldFrame, +} + +impl VideoQueue { + /// Creates the bounded queue and the render-thread reader draining it. + pub(super) fn channel() -> (Self, VideoReader) { + let (tx, rx) = bounded(VIDEO_QUEUE_CAP); + let held = Arc::new(Mutex::new(None)); + ( + Self { + tx, + drain: rx.clone(), + held: Arc::clone(&held), + }, + VideoReader { rx, held }, + ) + } + + /// Queues a frame for presentation; gives it back when the queue is + /// full. + fn try_push(&self, frame: VideoFrame) -> Result<(), VideoFrame> { + self.tx.try_send(frame).map_err(TrySendError::into_inner) + } + + /// Discards every buffered frame, including the one the reader holds. + /// Keeping the slot locked across the drain stops the reader from + /// picking up a pre-flush frame halfway through. + fn flush(&self) { + let mut held = self.held.lock(); + held.take(); + while self.drain.try_recv().is_ok() {} + } + + /// Whether the render thread has consumed every queued frame. + fn is_drained(&self) -> bool { + self.tx.is_empty() && self.held.lock().is_none() + } +} + +/// Render-thread half of the presentation queue. +pub(super) struct VideoReader { + rx: Receiver, + held: HeldFrame, +} + +impl VideoReader { + /// [render thread] The frame to present at `now`: drops frames whose + /// time already passed, returns the most recent due one, and keeps an + /// undue frame held for a later call. + pub(super) fn next_due(&self, now: f64) -> Option { + let mut held = self.held.lock(); + let mut due = None; + loop { + if held.is_none() { + match self.rx.try_recv() { + Ok(frame) => *held = Some(frame), + Err(_) => break, + } + } + match held.as_ref() { + Some(frame) if frame.pts().is_none_or(|pts| pts <= now) => { + due = held.take(); + } + _ => break, + } + } + due + } +} + +/// The video half of a playback: decodes the video stream and fills the +/// presentation queue the render thread consumes. +pub(super) struct VideoPlayback { + decoder: VideoDecoder, + queue: VideoQueue, + stream_index: c_int, +} + +impl VideoPlayback { + pub(super) fn new( + stream: Stream, + stream_index: c_int, + hw: &HwDeviceContext, + queue: VideoQueue, + ) -> Result { + Ok(Self { + decoder: VideoDecoder::new(stream, hw)?, + queue, + stream_index, + }) + } + + pub(super) const fn handles(&self, stream_index: c_int) -> bool { + self.stream_index == stream_index + } + + /// Sends the packet and moves every ready frame into the queue. + pub(super) fn handle_packet( + &mut self, + packet: &mut OwnedPacket, + start_offset: f64, + cancel: &ReadOnlyCancelToken, + seek: &AtomicSeekSlot, + poll_controls: &dyn Fn(), + ) -> Result<()> { + self.decoder.send(packet.as_mut_ptr())?; + self.pump(start_offset, cancel, seek, poll_controls) + } + + /// Sends end-of-stream and moves the remaining frames into the queue. + pub(super) fn drain( + &mut self, + start_offset: f64, + cancel: &ReadOnlyCancelToken, + seek: &AtomicSeekSlot, + poll_controls: &dyn Fn(), + ) -> Result<()> { + self.decoder.send(ptr::null())?; + self.pump(start_offset, cancel, seek, poll_controls) + } + + /// Discards everything belonging to the pre-seek position. + pub(super) fn flush_for_seek(&mut self) { + self.decoder.flush(); + self.queue.flush(); + } + + /// Whether the render thread has consumed every queued frame. + pub(super) fn is_drained(&self) -> bool { + self.queue.is_drained() + } + + /// Moves every frame the decoder has ready into the presentation queue, + /// waiting for space while staying responsive to stop/seek commands. + /// The wait also keeps `poll_controls` applied: while not playing the + /// queue does not drain, so a queued play would otherwise never land. + fn pump( + &mut self, + start_offset: f64, + cancel: &ReadOnlyCancelToken, + seek: &AtomicSeekSlot, + poll_controls: &dyn Fn(), + ) -> Result<()> { + loop { + match self.decoder.receive()? { + Decoded::Frame(mut frame) => { + frame.shift_pts(start_offset); + loop { + if cancel.is_cancelled() || seek.is_pending() { + // the frame is obsolete: the queue is about to + // be flushed + return Ok(()); + } + // after the cancel check: a retired unit must not + // consume a command meant for its successor + poll_controls(); + match self.queue.try_push(frame) { + Ok(()) => break, + Err(returned) => { + frame = returned; + thread::sleep(PLAYBACK_POLL); + } + } + } + } + Decoded::Again | Decoded::Eof => return Ok(()), + } + } + } +} diff --git a/Explorer/Assets/Plugins/UUAV/native/src/player.rs b/Explorer/Assets/Plugins/UUAV/native/src/player.rs new file mode 100644 index 00000000000..abd281a6b42 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/src/player.rs @@ -0,0 +1,304 @@ +use anyhow::{Result, anyhow}; +use arc_swap::ArcSwap; +use std::sync::Arc; +use std::thread::{self}; + +use crossbeam_channel::{Receiver, Sender, TrySendError, bounded}; + +use crate::ffutil::StreamingProtocol; +use crate::hw_device::HwDevice; +use crate::playback::{ + CancelToken, ControlPush, DEFAULT_PLAYBACK_RATE, PlaybackUnit, ReadOnlyCancelToken, + UnitControls, fill_silence, +}; +use crate::video_output::VideoTextureView; +use crate::{AudioOptionsView, ControlsState, ErrorCallback, MediaInfo, UUAVState, VideoSize}; + +/// Lifecycle of the player's playback, shared with the playback thread. +enum Playback { + /// Nothing: no media, no thread. + Closed, + /// The playback thread exists but has not opened the media yet. + Opening, + /// The open or the playback failed; the error already went through + /// the callback and the unit is gone. + Failed, + /// The media is open; the playback thread runs this unit. + Active(Arc), +} + +type Url = String; + +type OpenIntent = (Url, ReadOnlyCancelToken); + +/// Engine-facing player: manages url switching by spawning a fresh +/// playback thread per opened url and delegates everything else to the +/// [`PlaybackUnit`] that thread publishes. +pub(crate) struct UUAVPlayer { + audio_out: AudioOptionsView, + playback: Arc>, + play_control: ControlPush, + looping_control: ControlPush, + rate_control: ControlPush, + sender_open_intent: Sender, + receiver_open_intent: Receiver, + last_cancel_token: Option, +} + +impl Drop for UUAVPlayer { + fn drop(&mut self) { + self.close_media(); + } +} + +impl UUAVPlayer { + pub(crate) fn new( + id: crate::PlayerId, + device: HwDevice, + audio_out: AudioOptionsView, + error_callback: ErrorCallback, + protocol_whitelist: Arc, + ) -> Result { + let (sender_open_intent, receiver_open_intent) = bounded::(1); + let playback = Arc::new(ArcSwap::from_pointee(Playback::Closed)); + let play_control = ControlPush::new(false); + let looping_control = ControlPush::new(false); + let rate_control = ControlPush::new(DEFAULT_PLAYBACK_RATE); + + let spawned = thread::Builder::new() + .name(format!("uuav-player-{id}")) + .spawn({ + let audio_out = audio_out.clone(); + let playback = playback.clone(); + let receiver = receiver_open_intent.clone(); + let play_or_pause = play_control.consumer(); + let looping = looping_control.consumer(); + let rate = rate_control.consumer(); + + move || { + while let Ok((url, cancel_token)) = receiver.recv() { + playback.store(Arc::new(Playback::Opening)); + + match PlaybackUnit::open( + url, + device.clone(), + audio_out.clone(), + cancel_token.clone(), + error_callback.clone(), + &protocol_whitelist, + UnitControls { + play_or_pause: play_or_pause.clone(), + looping: looping.clone(), + rate: rate.clone(), + }, + ) { + Ok((unit, pipeline)) => { + let unit = Arc::new(unit); + playback.store(Arc::new(Playback::Active(Arc::clone(&unit)))); + if let Err(e) = unit.run_blocking(pipeline) { + error_callback.report(e.to_string()); + playback.store(Arc::new(Playback::Failed)); + } else { + playback.store(Arc::new(Playback::Closed)); + } + } + Err(e) => { + if cancel_token.is_cancelled() { + playback.store(Arc::new(Playback::Closed)); + } else { + error_callback.report(e.to_string()); + playback.store(Arc::new(Playback::Failed)); + } + } + } + } + } + }); + + match spawned { + // Safe to do not join the handle on drop + Ok(_handle) => Ok(Self { + audio_out, + playback, + play_control, + looping_control, + rate_control, + sender_open_intent, + receiver_open_intent, + last_cancel_token: None, + }), + Err(e) => Err(anyhow!("failed to spawn playback thread: {e}")), + } + } + + // not blocking + // async, returns immediately + pub(crate) fn open_media_intent(&mut self, url: String) -> Result<()> { + const ATTEMPTS: u32 = 100; + + self.close_media(); + + let cancel = CancelToken::new(); + self.last_cancel_token = Some(cancel.clone()); + + let mut payload: OpenIntent = (url, cancel.into()); + + // Send, discard the oldest one if exists + for _ in 0..ATTEMPTS { + match self.sender_open_intent.try_send(payload) { + Ok(()) => return Ok(()), + Err(TrySendError::Full(recovery)) => { + payload = recovery; + let _ = self.receiver_open_intent.try_recv(); + } + Err(TrySendError::Disconnected(_)) => { + return Err(anyhow!( + "Instance is disposed, should never happen but no hard fall with unreachable" + )); + } + } + } + + Err(anyhow!("Ran out of attempts: {ATTEMPTS}")) + } + + pub(crate) fn close_media(&mut self) { + // Prevent unrequired autoplay on open_media + self.play_control.push(false); + + if let Some(cancel_token) = self.last_cancel_token.take() { + cancel_token.cancel(); + self.playback.rcu(|cur| { + if matches!(cur.as_ref(), Playback::Failed) { + Arc::new(Playback::Closed) + } else { + cur.clone() + } + }); + } + } + + fn unit(&self) -> Option> { + match self.playback.load().as_ref() { + Playback::Active(unit) => Some(Arc::clone(unit)), + Playback::Closed | Playback::Opening | Playback::Failed => None, + } + } + + fn active_unit(&self) -> Result> { + self.unit() + .ok_or_else(|| anyhow!("no media open: {:?}", self.state())) + } + + pub(crate) fn play(&self) -> Result<()> { + self.play_control.push(true); + Ok(()) + } + + pub(crate) fn pause(&self) -> Result<()> { + self.play_control.push(false); + Ok(()) + } + + pub(crate) fn set_looping(&self, looping: bool) { + self.looping_control.push(looping); + } + + pub(crate) fn looping(&self) -> bool { + self.looping_control.latest() + } + + pub(crate) fn controls_state(&self) -> ControlsState { + let (play, play_pending) = self.play_control.snapshot(); + let (looping, looping_pending) = self.looping_control.snapshot(); + let (rate, rate_pending) = self.rate_control.snapshot(); + ControlsState { + rate, + play: u8::from(play), + play_pending: u8::from(play_pending), + looping: u8::from(looping), + looping_pending: u8::from(looping_pending), + rate_pending: u8::from(rate_pending), + } + } + + /// Varispeed rate in media seconds per wall second; persists across + /// url switches. Realtime streams keep playing at 1x regardless. + pub(crate) fn set_rate(&self, rate: f64) { + self.rate_control.push(rate); + } + + pub(crate) fn rate(&self) -> f64 { + self.rate_control.latest() + } + + // not blocking + pub(crate) fn seek_intent(&self, time: f64) -> Result<()> { + self.active_unit()?.seek_intent(time); + Ok(()) + } + + pub(crate) fn state(&self) -> UUAVState { + match self.playback.load().as_ref() { + Playback::Closed => UUAVState::UUAV_CLOSED, + Playback::Opening => UUAVState::UUAV_OPENING, + Playback::Failed => UUAVState::UUAV_ERROR, + Playback::Active(unit) => unit.state(), + } + } + + pub(crate) fn duration(&self) -> Option { + self.unit()?.duration() + } + + pub(crate) fn current_time(&self) -> Option { + Some(self.unit()?.current_time()) + } + + // the player slaves its playback to the externally provided master clock + pub(crate) fn assign_master_clock(&self, current_time: f64) -> Result<()> { + self.active_unit()?.assign_master_clock(current_time); + Ok(()) + } + + pub(crate) fn video_size(&self) -> Option { + self.unit()?.video_size() + } + + pub(crate) fn media_info(&self) -> Option { + Some(self.unit()?.media_info()) + } + + pub(crate) fn video_texture( + &self, + #[cfg(target_os = "macos")] plane: i32, + ) -> Option { + self.unit()?.video_texture( + #[cfg(target_os = "macos")] + plane, + ) + } + + // [render thread] presents the frame that is due per the clock + pub(crate) fn on_render_event(&self) { + if let Some(unit) = self.unit() { + unit.on_render_event(); + } + } + + // [audio thread] fills interleaved FLT; pads silence on underrun, + // never blocks; returns frames actually copied + pub(crate) fn read_audio(&self, dst: *mut f32, nb_frames: i32) -> i32 { + let Ok(frames) = usize::try_from(nb_frames) else { + return 0; + }; + match self.unit() { + Some(unit) => unit.read_audio(dst, frames), + None => { + // no media (or still opening): silence in the engine's layout + fill_silence(dst, frames, self.audio_out.current().channels_usize()); + 0 + } + } + } +} diff --git a/Explorer/Assets/Plugins/UUAV/native/src/video_decoder_macos.rs b/Explorer/Assets/Plugins/UUAV/native/src/video_decoder_macos.rs new file mode 100644 index 00000000000..769dee0a51c --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/src/video_decoder_macos.rs @@ -0,0 +1,207 @@ +// TODO merge it with windows implementation, a lot of similar parts just repeats + +use anyhow::{Context, Result, anyhow}; +use ffmpeg_sys_next as ff; +use std::ffi::CStr; +use std::os::raw::{c_int, c_void}; +use std::ptr; + +use crate::ffutil::{ + AVERROR_EAGAIN, Decoded, OwnedDecoder, OwnedFrame, Stream, av_err, check, q2d, +}; +use crate::hw_device::HwDeviceContext; + +/// A decoded VideoToolbox frame. The `CVPixelBufferRef` behind it stays +/// alive (and thus valid for GPU reads) for as long as `frame` is held: +/// AVFrame refcounting owns the CF retain, so handing frames from the +/// playback thread to the render thread needs no extra retain/release. +pub(crate) struct VideoFrame { + frame: OwnedFrame, + pts: Option, + width: i32, + height: i32, +} + +impl VideoFrame { + /// `CVPixelBufferRef` of the decoded frame (IOSurface-backed NV12). + pub(crate) fn pixel_buffer(&self) -> *mut c_void { + self.frame.data(3).cast::() + } + + /// Presentation time in seconds of the media timeline, `None` when the + /// container provides no usable timestamp. + pub(crate) const fn pts(&self) -> Option { + self.pts + } + + /// Shifts the presentation time so the media timeline starts at zero. + pub(crate) fn shift_pts(&mut self, offset: f64) { + self.pts = self.pts.map(|pts| pts - offset); + } + + pub(crate) const fn width(&self) -> i32 { + self.width + } + + pub(crate) const fn height(&self) -> i32 { + self.height + } +} + +/// VideoToolbox hardware video decoder. Hardware decoding is mandatory: the +/// stream is rejected if the codec offers no VideoToolbox path or the +/// decoded surfaces are not NV12. +pub(crate) struct VideoDecoder { + ctx: OwnedDecoder, + time_base: ff::AVRational, +} + +impl VideoDecoder { + /// Extra surfaces the decoder allocates on top of its own needs; must + /// cover every frame the player can hold queued at once. + pub(crate) const EXTRA_HW_FRAMES: c_int = 8; + + pub(crate) fn new(stream: Stream, hw: &HwDeviceContext) -> Result { + let codec = stream.find_decoder().context("video stream")?; + ensure_videotoolbox_support(codec)?; + let mut ctx = OwnedDecoder::new(codec)?; + + unsafe { + let raw = ctx.as_mut_ptr(); + check( + "avcodec_parameters_to_context(video)", + ff::avcodec_parameters_to_context(raw, stream.codecpar()), + )?; + (*raw).pkt_timebase = stream.time_base(); + (*raw).hw_device_ctx = ff::av_buffer_ref(hw.as_buffer_ptr()); + if (*raw).hw_device_ctx.is_null() { + return Err(anyhow!( + "failed to reference the VideoToolbox device context" + )); + } + (*raw).get_format = Some(require_videotoolbox_format); + (*raw).extra_hw_frames = Self::EXTRA_HW_FRAMES; + + check( + "avcodec_open2(video)", + ff::avcodec_open2(raw, codec, ptr::null_mut()), + )?; + } + + Ok(Self { + ctx, + time_base: stream.time_base(), + }) + } + + /// Sends a packet; pass `null` to signal end of stream. + pub(crate) fn send(&mut self, packet: *const ff::AVPacket) -> Result<()> { + let ret = unsafe { ff::avcodec_send_packet(self.ctx.as_mut_ptr(), packet) }; + // EOF from send just means the drain was already signalled + if ret < 0 && ret != ff::AVERROR_EOF { + return Err(av_err("avcodec_send_packet(video)", ret)); + } + Ok(()) + } + + pub(crate) fn receive(&mut self) -> Result> { + let mut frame = OwnedFrame::new()?; + let ret = unsafe { ff::avcodec_receive_frame(self.ctx.as_mut_ptr(), frame.as_mut_ptr()) }; + if ret == AVERROR_EAGAIN { + return Ok(Decoded::Again); + } + if ret == ff::AVERROR_EOF { + return Ok(Decoded::Eof); + } + check("avcodec_receive_frame(video)", ret)?; + + if frame.format() != ff::AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX as c_int { + return Err(anyhow!( + "decoder produced a non-VideoToolbox frame (format {}); hardware decoding is mandatory", + frame.format() + )); + } + + // sw_format NV12 covers both the video- and full-range CV pixel + // formats; anything else (P010/10-bit, ...) is rejected, matching + // the NV12-only policy of the D3D11 sibling + let sw_format = frame + .hw_frames_ctx() + .context("VideoToolbox frame")? + .sw_format; + if sw_format != ff::AVPixelFormat::AV_PIX_FMT_NV12 { + return Err(anyhow!( + "unsupported VideoToolbox surface format {:?}; only NV12 is supported", + sw_format + )); + } + + let ts = frame.best_effort_timestamp(); + let pts = if ts == ff::AV_NOPTS_VALUE { + None + } else { + Some(ts as f64 * q2d(self.time_base)) + }; + let width = frame.width(); + let height = frame.height(); + + Ok(Decoded::Frame(VideoFrame { + frame, + pts, + width, + height, + })) + } + + pub(crate) fn flush(&mut self) { + unsafe { ff::avcodec_flush_buffers(self.ctx.as_mut_ptr()) }; + } +} + +/// Rejects codecs without a VideoToolbox hardware path. +/// Software-only decoders never invoke `get_format`, so without +/// this probe the rejection would only happen after a software frame was +/// already decoded. +/// TODO support software fallback next +fn ensure_videotoolbox_support(codec: *const ff::AVCodec) -> Result<()> { + for index in 0.. { + let config = unsafe { ff::avcodec_get_hw_config(codec, index) }; + if config.is_null() { + break; + } + let config = unsafe { &*config }; + let by_device_ctx = + config.methods & ff::AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX as c_int != 0; + if by_device_ctx + && config.device_type == ff::AVHWDeviceType::AV_HWDEVICE_TYPE_VIDEOTOOLBOX + && config.pix_fmt == ff::AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX + { + return Ok(()); + } + } + + let name = unsafe { CStr::from_ptr(ff::avcodec_get_name((*codec).id)) }; + Err(anyhow!( + "codec {} has no VideoToolbox hardware decoder; hardware decoding is mandatory", + name.to_string_lossy() + )) +} + +/// Rejects everything but the VideoToolbox hardware pixel format: returning +/// NONE makes `avcodec` fail decoding instead of silently falling back to +/// software frames. +unsafe extern "C" fn require_videotoolbox_format( + _ctx: *mut ff::AVCodecContext, + list: *const ff::AVPixelFormat, +) -> ff::AVPixelFormat { + unsafe { + let mut p = list; + while !p.is_null() && *p != ff::AVPixelFormat::AV_PIX_FMT_NONE { + if *p == ff::AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX { + return ff::AVPixelFormat::AV_PIX_FMT_VIDEOTOOLBOX; + } + p = p.add(1); + } + } + ff::AVPixelFormat::AV_PIX_FMT_NONE +} diff --git a/Explorer/Assets/Plugins/UUAV/native/src/video_decoder_windows.rs b/Explorer/Assets/Plugins/UUAV/native/src/video_decoder_windows.rs new file mode 100644 index 00000000000..db72208b8dc --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/src/video_decoder_windows.rs @@ -0,0 +1,201 @@ +use anyhow::{Context, Result, anyhow}; +use ffmpeg_sys_next as ff; +use std::ffi::CStr; +use std::os::raw::{c_int, c_void}; +use std::ptr; + +use crate::ffutil::{ + AVERROR_EAGAIN, Decoded, OwnedDecoder, OwnedFrame, Stream, av_err, check, q2d, +}; +use crate::hw_device::HwDeviceContext; + +/// A decoded D3D11VA frame. The underlying decoder surface stays alive +/// (and thus valid for GPU copies) for as long as `frame` is held. +pub(crate) struct VideoFrame { + frame: OwnedFrame, + pts: Option, + width: i32, + height: i32, +} + +impl VideoFrame { + /// `ID3D11Texture2D*` of the decoder surface pool. + pub(crate) fn texture_raw(&self) -> *mut c_void { + self.frame.data(0).cast::() + } + + /// Array slice index inside the decoder's texture array. + pub(crate) fn subresource(&self) -> u32 { + let index = self.frame.data(1) as usize; + u32::try_from(index).unwrap_or(0) + } + + /// Presentation time in seconds of the media timeline, `None` when the + /// container provides no usable timestamp. + pub(crate) const fn pts(&self) -> Option { + self.pts + } + + /// Shifts the presentation time so the media timeline starts at zero. + pub(crate) fn shift_pts(&mut self, offset: f64) { + self.pts = self.pts.map(|pts| pts - offset); + } + + pub(crate) const fn width(&self) -> i32 { + self.width + } + + pub(crate) const fn height(&self) -> i32 { + self.height + } +} + +/// D3D11VA hardware video decoder. Hardware decoding is mandatory: the +/// stream is rejected if the codec offers no D3D11 path or the surfaces +/// are not NV12. +pub(crate) struct VideoDecoder { + ctx: OwnedDecoder, + time_base: ff::AVRational, +} + +impl VideoDecoder { + /// Extra surfaces the decoder allocates on top of its own needs; must + /// cover every frame the player can hold queued at once. + pub(crate) const EXTRA_HW_FRAMES: c_int = 8; + + pub(crate) fn new(stream: Stream, hw: &HwDeviceContext) -> Result { + let codec = stream.find_decoder().context("video stream")?; + ensure_d3d11_support(codec)?; + let mut ctx = OwnedDecoder::new(codec)?; + + unsafe { + let raw = ctx.as_mut_ptr(); + check( + "avcodec_parameters_to_context(video)", + ff::avcodec_parameters_to_context(raw, stream.codecpar()), + )?; + (*raw).pkt_timebase = stream.time_base(); + (*raw).hw_device_ctx = ff::av_buffer_ref(hw.as_buffer_ptr()); + if (*raw).hw_device_ctx.is_null() { + return Err(anyhow!("failed to reference the D3D11 device context")); + } + (*raw).get_format = Some(require_d3d11_format); + (*raw).extra_hw_frames = Self::EXTRA_HW_FRAMES; + + check( + "avcodec_open2(video)", + ff::avcodec_open2(raw, codec, ptr::null_mut()), + )?; + } + + Ok(Self { + ctx, + time_base: stream.time_base(), + }) + } + + /// Sends a packet; pass `null` to signal end of stream. + pub(crate) fn send(&mut self, packet: *const ff::AVPacket) -> Result<()> { + let ret = unsafe { ff::avcodec_send_packet(self.ctx.as_mut_ptr(), packet) }; + // EOF from send just means the drain was already signalled + if ret < 0 && ret != ff::AVERROR_EOF { + return Err(av_err("avcodec_send_packet(video)", ret)); + } + Ok(()) + } + + pub(crate) fn receive(&mut self) -> Result> { + let mut frame = OwnedFrame::new()?; + let ret = unsafe { ff::avcodec_receive_frame(self.ctx.as_mut_ptr(), frame.as_mut_ptr()) }; + if ret == AVERROR_EAGAIN { + return Ok(Decoded::Again); + } + if ret == ff::AVERROR_EOF { + return Ok(Decoded::Eof); + } + check("avcodec_receive_frame(video)", ret)?; + + if frame.format() != ff::AVPixelFormat::AV_PIX_FMT_D3D11 as c_int { + return Err(anyhow!( + "decoder produced a non-D3D11 frame (format {}); hardware decoding is mandatory", + frame.format() + )); + } + + let sw_format = frame.hw_frames_ctx().context("D3D11 frame")?.sw_format; + if sw_format != ff::AVPixelFormat::AV_PIX_FMT_NV12 { + return Err(anyhow!( + "unsupported D3D11 surface format {:?}; only NV12 is supported", + sw_format + )); + } + + let ts = frame.best_effort_timestamp(); + let pts = if ts == ff::AV_NOPTS_VALUE { + None + } else { + Some(ts as f64 * q2d(self.time_base)) + }; + let width = frame.width(); + let height = frame.height(); + + Ok(Decoded::Frame(VideoFrame { + frame, + pts, + width, + height, + })) + } + + pub(crate) fn flush(&mut self) { + unsafe { ff::avcodec_flush_buffers(self.ctx.as_mut_ptr()) }; + } +} + +/// Rejects codecs without a D3D11VA hardware path +/// Software-only decoders never invoke `get_format`, so without +/// this probe the rejection would only happen after a software frame was +/// already decoded. +/// TODO support software fallback next +fn ensure_d3d11_support(codec: *const ff::AVCodec) -> Result<()> { + for index in 0.. { + let config = unsafe { ff::avcodec_get_hw_config(codec, index) }; + if config.is_null() { + break; + } + let config = unsafe { &*config }; + let by_device_ctx = + config.methods & ff::AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX as c_int != 0; + if by_device_ctx + && config.device_type == ff::AVHWDeviceType::AV_HWDEVICE_TYPE_D3D11VA + && config.pix_fmt == ff::AVPixelFormat::AV_PIX_FMT_D3D11 + { + return Ok(()); + } + } + + let name = unsafe { CStr::from_ptr(ff::avcodec_get_name((*codec).id)) }; + Err(anyhow!( + "codec {} has no D3D11 hardware decoder; hardware decoding is mandatory", + name.to_string_lossy() + )) +} + +/// Rejects everything but the D3D11 hardware pixel format: returning NONE +/// makes `avcodec` fail decoding instead of silently falling back to +/// software frames. +unsafe extern "C" fn require_d3d11_format( + _ctx: *mut ff::AVCodecContext, + list: *const ff::AVPixelFormat, +) -> ff::AVPixelFormat { + unsafe { + let mut p = list; + while !p.is_null() && *p != ff::AVPixelFormat::AV_PIX_FMT_NONE { + if *p == ff::AVPixelFormat::AV_PIX_FMT_D3D11 { + return ff::AVPixelFormat::AV_PIX_FMT_D3D11; + } + p = p.add(1); + } + } + ff::AVPixelFormat::AV_PIX_FMT_NONE +} diff --git a/Explorer/Assets/Plugins/UUAV/native/src/video_output_macos.rs b/Explorer/Assets/Plugins/UUAV/native/src/video_output_macos.rs new file mode 100644 index 00000000000..383db87470d --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/src/video_output_macos.rs @@ -0,0 +1,242 @@ +use anyhow::{Result, anyhow}; +use objc2::Message as _; +use objc2::rc::Retained; +use objc2::runtime::ProtocolObject; +use objc2_core_video::{CVPixelBuffer, CVPixelBufferGetIOSurface}; +use objc2_io_surface::IOSurfaceRef; +use objc2_metal::{ + MTLBlitCommandEncoder, MTLCommandBuffer, MTLCommandEncoder, MTLCommandQueue, MTLDevice, + MTLOrigin, MTLPixelFormat, MTLSize, MTLStorageMode, MTLTexture, MTLTextureDescriptor, + MTLTextureUsage, +}; +use std::os::raw::c_void; + +use crate::hw_device::HwDevice; +use crate::video_decoder::VideoFrame; + +type Texture = Retained>; + +/// Non-owning view of one of [`VideoOutput`]'s presentation planes: one +/// `MTLTexture` per plane (0 = Y `R8Unorm`, 1 = UV `RG8Unorm`). The retain +/// held here only pins the texture while the view lives; `VideoOutput` +/// stays the owner and recreates both planes together on a resolution +/// change, after which consumers re-query both (contract of +/// `uuav_player_get_video_texture`). +pub(crate) struct VideoTextureView { + texture: Texture, +} + +impl VideoTextureView { + pub(crate) fn raw_ptr_mut(&self) -> *mut c_void { + Retained::as_ptr(&self.texture).cast_mut().cast::() + } +} + +/// Presentation target: two owned per-plane textures (Y `R8Unorm`, UV +/// `RG8Unorm`) on the engine-provided device that decoded frames are blitted +/// into on the render thread. The engine consumes the raw `id` +/// pointers directly - unlike D3D11 there is no multi-plane resource to +/// create views over, so each plane is its own texture. +pub(crate) struct VideoOutput { + device: Retained>, + queue: Retained>, + planes: Option, + /// Previous plane generation, kept alive for one resolution change so + /// the pointers the engine still wraps stay valid until its next poll + /// notices the new plane-0 pointer. + retired: Option, +} + +/// The plane textures together with the dimensions they were created for. +struct SizedPlanes { + y: Texture, + uv: Texture, + width: u32, + height: u32, +} + +impl SizedPlanes { + const fn matches(&self, width: u32, height: u32) -> bool { + self.width == width && self.height == height + } +} + +// The Metal objects are only used behind the player's mutex and Metal +// devices/queues/textures are free-threaded; Retained> +// merely lacks the marker because ObjC protocols leave it undeclared. +#[allow(clippy::non_send_fields_in_send_ty)] +unsafe impl Send for VideoOutput {} + +impl VideoOutput { + pub(crate) fn new(device: &HwDevice) -> Self { + Self { + device: device.metal_device().retain(), + queue: device.command_queue().retain(), + planes: None, + retired: None, + } + } + + /// One plane texture, once the first frame has been presented. + pub(crate) fn texture(&self, plane: i32) -> Option { + let planes = self.planes.as_ref()?; + let texture = match plane { + 0 => planes.y.clone(), + 1 => planes.uv.clone(), + _ => return None, + }; + Some(VideoTextureView { texture }) + } + + /// Takes no decode context on purpose (the D3D11 sibling needs one for + /// its immediate-context lock): the blit runs on the plugin's own queue + /// against an immutable decoded surface. + pub(crate) fn present(&mut self, frame: &VideoFrame) -> Result<()> { + // NV12 requires even dimensions; negative decoder sizes fold to 0 + let width = u32::try_from(frame.width()).unwrap_or(0) & !1; + let height = u32::try_from(frame.height()).unwrap_or(0) & !1; + if width == 0 || height == 0 { + return Err(anyhow!("frame has no presentable size")); + } + self.ensure_planes(width, height)?; + let planes = self + .planes + .as_ref() + .ok_or_else(|| anyhow!("presentation planes are missing"))?; + + let pixel_buffer = frame.pixel_buffer(); + if pixel_buffer.is_null() { + return Err(anyhow!("decoded frame has no CVPixelBuffer")); + } + let pixel_buffer: &CVPixelBuffer = unsafe { &*pixel_buffer.cast::() }; + + // FFmpeg's VideoToolbox hwcontext requests IOSurface backing, so a + // missing surface is a broken frame, not a fallback case + let surface = CVPixelBufferGetIOSurface(Some(pixel_buffer)) + .ok_or_else(|| anyhow!("CVPixelBuffer is not IOSurface-backed"))?; + + // zero-copy views over the decoder surface planes, at the padded + // (coded-size) dimensions the IOSurface actually has + let src_y = self.wrap_surface_plane(&surface, 0, MTLPixelFormat::R8Unorm)?; + let src_uv = self.wrap_surface_plane(&surface, 1, MTLPixelFormat::RG8Unorm)?; + + let buffer = self + .queue + .commandBuffer() + .ok_or_else(|| anyhow!("command queue returned no command buffer"))?; + let encoder = buffer + .blitCommandEncoder() + .ok_or_else(|| anyhow!("command buffer returned no blit encoder"))?; + + // the visible box only (Metal analog of the D3D11_BOX copy): the + // IOSurface planes can be padded past the visible size + let origin = MTLOrigin { x: 0, y: 0, z: 0 }; + unsafe { + encoder.copyFromTexture_sourceSlice_sourceLevel_sourceOrigin_sourceSize_toTexture_destinationSlice_destinationLevel_destinationOrigin( + &src_y, + 0, + 0, + origin, + MTLSize { + width: width as usize, + height: height as usize, + depth: 1, + }, + &planes.y, + 0, + 0, + origin, + ); + encoder.copyFromTexture_sourceSlice_sourceLevel_sourceOrigin_sourceSize_toTexture_destinationSlice_destinationLevel_destinationOrigin( + &src_uv, + 0, + 0, + origin, + MTLSize { + width: (width / 2) as usize, + height: (height / 2) as usize, + depth: 1, + }, + &planes.uv, + 0, + 0, + origin, + ); + } + encoder.endEncoding(); + buffer.commit(); + // synchronous on purpose: orders the copy against Unity's own queue + // and proves the borrowed frame/IOSurface outlives the GPU reads. + // Optimization headroom (MTLSharedEvent, IUnityGraphicsMetal) is + // deliberately deferred. + buffer.waitUntilCompleted(); + Ok(()) + } + + /// Ensures the owned plane textures match the size, recreating them on + /// change and retiring the previous generation for one more poll cycle. + fn ensure_planes(&mut self, width: u32, height: u32) -> Result<()> { + if self + .planes + .as_ref() + .is_some_and(|planes| planes.matches(width, height)) + { + return Ok(()); + } + + let y = self.new_plane(MTLPixelFormat::R8Unorm, width, height)?; + let uv = self.new_plane(MTLPixelFormat::RG8Unorm, width / 2, height / 2)?; + // the generation before last dies here; the engine has had a full + // poll cycle to stop wrapping it + let _previous = self.retired.take(); + self.retired = self.planes.take(); + self.planes = Some(SizedPlanes { + y, + uv, + width, + height, + }); + Ok(()) + } + + /// One owned destination plane the engine samples from. + fn new_plane(&self, format: MTLPixelFormat, width: u32, height: u32) -> Result { + let descriptor = unsafe { + MTLTextureDescriptor::texture2DDescriptorWithPixelFormat_width_height_mipmapped( + format, + width as usize, + height as usize, + false, + ) + }; + descriptor.setUsage(MTLTextureUsage::ShaderRead); + descriptor.setStorageMode(MTLStorageMode::Private); + self.device + .newTextureWithDescriptor(&descriptor) + .ok_or_else(|| anyhow!("newTexture({format:?} {width}x{height}) returned no texture")) + } + + /// Zero-copy `MTLTexture` view over one IOSurface plane of the decoded + /// frame. Chosen over `CVMetalTextureCache`: no cache object to own and + /// flush, no `CVMetalTextureRef` lifetime to manage. + fn wrap_surface_plane( + &self, + surface: &IOSurfaceRef, + plane: usize, + format: MTLPixelFormat, + ) -> Result { + let width = surface.width_of_plane(plane); + let height = surface.height_of_plane(plane); + let descriptor = unsafe { + MTLTextureDescriptor::texture2DDescriptorWithPixelFormat_width_height_mipmapped( + format, width, height, false, + ) + }; + // IOSurface-backed textures must be Shared (they alias CPU-visible + // memory); the default usage (ShaderRead) is fine for a blit source + descriptor.setStorageMode(MTLStorageMode::Shared); + self.device + .newTextureWithDescriptor_iosurface_plane(&descriptor, surface, plane) + .ok_or_else(|| anyhow!("wrapping IOSurface plane {plane} as {format:?} failed")) + } +} diff --git a/Explorer/Assets/Plugins/UUAV/native/src/video_output_windows.rs b/Explorer/Assets/Plugins/UUAV/native/src/video_output_windows.rs new file mode 100644 index 00000000000..c768ecaee10 --- /dev/null +++ b/Explorer/Assets/Plugins/UUAV/native/src/video_output_windows.rs @@ -0,0 +1,157 @@ +use anyhow::{Result, anyhow}; +use std::os::raw::c_void; +use windows::Win32::Graphics::Direct3D11::{ + D3D11_BIND_SHADER_RESOURCE, D3D11_BOX, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, ID3D11Device, + ID3D11DeviceContext, ID3D11Resource, ID3D11Texture2D, +}; +use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_NV12, DXGI_SAMPLE_DESC}; +use windows::core::Interface; + +use crate::hw_device::{HwDevice, HwDeviceContext}; +use crate::video_decoder::VideoFrame; + +/// Non-owning view of [`VideoOutput`]'s presentation texture: one NV12 +/// `ID3D11Texture2D` covering both planes, whatever plane was asked for - +/// the engine creates its own format-cast views over it (R8 -> Y, +/// R8G8 -> UV). The COM reference held here only pins the resource while +/// the view lives; `VideoOutput` stays the owner and recreates the texture +/// on resolution change, after which consumers re-query (contract of +/// `uuav_player_get_video_texture`). +pub(crate) struct VideoTextureView { + texture: ID3D11Texture2D, +} + +impl VideoTextureView { + pub(crate) fn raw_ptr_mut(&self) -> *mut c_void { + self.texture.as_raw() + } +} + +/// Presentation target: an NV12 texture on the engine-provided device that +/// decoded frames are copied into on the render thread. The engine consumes +/// the raw `ID3D11Texture2D*` and creates its own views over the planes. +pub(crate) struct VideoOutput { + device: ID3D11Device, + texture: Option, +} + +/// The NV12 texture together with the dimensions it was created for. +struct SizedTexture { + texture: ID3D11Texture2D, + width: u32, + height: u32, +} + +impl SizedTexture { + const fn matches(&self, width: u32, height: u32) -> bool { + self.width == width && self.height == height + } +} + +// The COM pointers are only used behind the player's mutex and D3D11 +// devices are free-threaded. +unsafe impl Send for VideoOutput {} + +impl VideoOutput { + pub(crate) fn new(device: &HwDevice) -> Self { + Self { + device: device.device().clone(), + texture: None, + } + } + + /// The shared NV12 texture, once the first frame has been presented. + pub(crate) fn texture(&self) -> Option { + self.texture.as_ref().map(|sized| VideoTextureView { + texture: sized.texture.clone(), + }) + } + + pub(crate) fn present(&mut self, hw: &HwDeviceContext, frame: &VideoFrame) -> Result<()> { + // NV12 requires even dimensions; negative decoder sizes fold to 0 + let width = u32::try_from(frame.width()).unwrap_or(0) & !1; + let height = u32::try_from(frame.height()).unwrap_or(0) & !1; + if width == 0 || height == 0 { + return Err(anyhow!("frame has no presentable size")); + } + let texture = self.ensure_texture(width, height)?; + + let src_raw = frame.texture_raw(); + let src = unsafe { ID3D11Resource::from_raw_borrowed(&src_raw) } + .ok_or_else(|| anyhow!("decoded frame has no D3D11 texture"))?; + + let ctx_raw = hw.immediate_context_raw(); + let ctx = unsafe { ID3D11DeviceContext::from_raw_borrowed(&ctx_raw) } + .ok_or_else(|| anyhow!("hw device context has no immediate context"))?; + + // The decoder surface can be part of a larger texture array with + // padded (coded-size) dimensions; copy just the visible box. + let src_box = D3D11_BOX { + left: 0, + top: 0, + front: 0, + right: width, + bottom: height, + back: 1, + }; + + // FFmpeg's own decoder threads drive the same immediate context; + // its device lock serializes us against them. + let _guard = hw.lock(); + unsafe { + ctx.CopySubresourceRegion( + texture, + 0, + 0, + 0, + 0, + src, + frame.subresource(), + Some(&src_box), + ); + } + Ok(()) + } + + /// Returns the presentation texture, recreating it when the size + /// changed or none exists yet. + fn ensure_texture(&mut self, width: u32, height: u32) -> Result<&ID3D11Texture2D> { + let texture = match self.texture.take() { + Some(existing) if existing.matches(width, height) => existing, + // TODO push a resolution-change notification to the consumer; + // for now C# polls texture ptr + size each frame (UUAVPlayer) + // and re-wraps after the pointer changes + _ => { + let desc = D3D11_TEXTURE2D_DESC { + Width: width, + Height: height, + MipLevels: 1, + ArraySize: 1, + Format: DXGI_FORMAT_NV12, + SampleDesc: DXGI_SAMPLE_DESC { + Count: 1, + Quality: 0, + }, + Usage: D3D11_USAGE_DEFAULT, + BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32, + CPUAccessFlags: 0, + MiscFlags: 0, + }; + + let mut created: Option = None; + unsafe { self.device.CreateTexture2D(&desc, None, Some(&mut created)) } + .map_err(|e| anyhow!("CreateTexture2D(NV12 {width}x{height}) failed: {e}"))?; + + let created = created.ok_or_else(|| { + anyhow!("CreateTexture2D(NV12 {width}x{height}) returned no texture") + })?; + SizedTexture { + texture: created, + width, + height, + } + } + }; + Ok(&self.texture.insert(texture).texture) + } +} diff --git a/Explorer/Packages/manifest.json b/Explorer/Packages/manifest.json index 02e7dab7919..61496ec413e 100644 --- a/Explorer/Packages/manifest.json +++ b/Explorer/Packages/manifest.json @@ -22,7 +22,6 @@ "com.mackysoft.serializereference-extensions": "1.3.1", "com.nickkhalow.chrome-devtool-protocol-unity": "https://github.com/decentraland/chrome-devtool-protocol-unity.git?path=/Packages", "com.nickkhalow.richtypes": "https://github.com/NickKhalow/RichTypesUnity.git?path=/Packages/RichTypes", - "com.renderheads.avpro.video-ultra": "git@github.com:decentraland/unity-explorer-packages.git?path=/AVProVideo", "com.unity.2d.sprite": "1.0.0", "com.unity.addressables": "2.9.1", "com.unity.animation.rigging": "1.4.1", diff --git a/Explorer/Packages/packages-lock.json b/Explorer/Packages/packages-lock.json index e0522e707d5..cee95af3b03 100644 --- a/Explorer/Packages/packages-lock.json +++ b/Explorer/Packages/packages-lock.json @@ -186,13 +186,6 @@ "dependencies": {}, "hash": "00cbf100acd16a024c6fb1acd2ba9a439dfa358b" }, - "com.renderheads.avpro.video-ultra": { - "version": "git@github.com:decentraland/unity-explorer-packages.git?path=/AVProVideo", - "depth": 0, - "source": "git", - "dependencies": {}, - "hash": "f79125eff7607f6d0e2665cb31eb313779908d55" - }, "com.unity.2d.sprite": { "version": "1.0.0", "depth": 0,