mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-01-15 17:59:22 -05:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
496 lines
9.7 KiB
C#
496 lines
9.7 KiB
C#
using NativeEngine;
|
|
using Sandbox.Audio;
|
|
|
|
namespace Sandbox;
|
|
|
|
/// <summary>
|
|
/// Enables video playback and access to the video texture and audio.
|
|
/// </summary>
|
|
public sealed class VideoPlayer : IDisposable, IWeakInteropHandle
|
|
{
|
|
internal CVideoPlayer native;
|
|
|
|
/// <summary>
|
|
/// Video successfully loaded.
|
|
/// </summary>
|
|
public Action OnLoaded { get; set; }
|
|
|
|
/// <summary>
|
|
/// Event that is invoked when the audio stream is created and ready to use.
|
|
/// </summary>
|
|
public Action OnAudioReady { get; set; }
|
|
|
|
/// <summary>
|
|
/// Video finished playing.
|
|
/// </summary>
|
|
public Action OnFinished { get; set; }
|
|
|
|
/// <summary>
|
|
/// Video started playing again after looping.
|
|
/// </summary>
|
|
public Action OnRepeated { get; set; }
|
|
|
|
public delegate void TextureChangedDelegate( ReadOnlySpan<byte> span, Vector2 size );
|
|
|
|
/// <summary>
|
|
/// If this event is set, texture data will be provided instead of rendering to the texture.
|
|
/// </summary>
|
|
public TextureChangedDelegate OnTextureData { get; set; }
|
|
|
|
/// <summary>
|
|
/// Sets whether the video should loop when it reaches the end.
|
|
/// </summary>
|
|
public bool Repeat
|
|
{
|
|
get => native.GetRepeat();
|
|
set => native.SetRepeat( value );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the total duration of the video in seconds.
|
|
/// </summary>
|
|
public float Duration => native.GetDuration();
|
|
|
|
/// <summary>
|
|
/// Gets the current playback time in seconds.
|
|
/// </summary>
|
|
public float PlaybackTime => native.GetPlaybackTime();
|
|
|
|
/// <summary>
|
|
/// Audio sample rate.
|
|
/// </summary>
|
|
public int SampleRate { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Number of audio channels.
|
|
/// </summary>
|
|
public int Channels { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Does the loaded video have audio?
|
|
/// </summary>
|
|
public bool HasAudio => native.HasAudioStream();
|
|
|
|
/// <summary>
|
|
/// Has the video been paused?
|
|
/// </summary>
|
|
public bool IsPaused => native.IsPaused();
|
|
|
|
/// <summary>
|
|
/// Texture of the video frame.
|
|
/// </summary>
|
|
public Texture Texture { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Width of the video.
|
|
/// </summary>
|
|
public int Width => native.GetWidth();
|
|
|
|
/// <summary>
|
|
/// Height of the video.
|
|
/// </summary>
|
|
public int Height => native.GetHeight();
|
|
|
|
uint IWeakInteropHandle.InteropHandle { get; set; }
|
|
|
|
private SoundHandle Sound;
|
|
|
|
/// <summary>
|
|
/// Access audio properties for this video playback.
|
|
/// </summary>
|
|
public AudioAccessor Audio { get; internal init; }
|
|
|
|
public class AudioAccessor
|
|
{
|
|
private readonly VideoPlayer Player;
|
|
private SoundHandle Sound => Player.Sound;
|
|
|
|
private bool listenLocal = true;
|
|
private Vector3 position = Vector3.Forward * 64.0f;
|
|
private float volume = 1.0f;
|
|
private bool lipSync = false;
|
|
private Mixer targetMixer;
|
|
|
|
internal AudioAccessor( VideoPlayer player )
|
|
{
|
|
Player = player;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Place the listener at 0,0,0 facing 1,0,0.
|
|
/// </summary>
|
|
public bool ListenLocal
|
|
{
|
|
get => listenLocal;
|
|
set
|
|
{
|
|
listenLocal = value;
|
|
if ( Sound.IsValid() && Sound.IsPlaying )
|
|
Sound.ListenLocal = listenLocal;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Position of the sound.
|
|
/// </summary>
|
|
public Vector3 Position
|
|
{
|
|
get => position;
|
|
set
|
|
{
|
|
position = value;
|
|
if ( Sound.IsValid() && Sound.IsPlaying )
|
|
Sound.Position = position;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Which mixer do we want to write to
|
|
/// </summary>
|
|
public Mixer TargetMixer
|
|
{
|
|
get => targetMixer;
|
|
set
|
|
{
|
|
if ( value == targetMixer )
|
|
return;
|
|
|
|
targetMixer = value;
|
|
if ( Sound.IsValid() )
|
|
Sound.TargetMixer = targetMixer;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Volume of the sound.
|
|
/// </summary>
|
|
public float Volume
|
|
{
|
|
get => volume;
|
|
set
|
|
{
|
|
if ( value == volume )
|
|
return;
|
|
|
|
volume = value;
|
|
if ( Sound.IsValid() )
|
|
Sound.Volume = volume;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enables lipsync processing.
|
|
/// </summary>
|
|
public bool LipSync
|
|
{
|
|
get => lipSync;
|
|
set
|
|
{
|
|
if ( value == lipSync )
|
|
return;
|
|
|
|
lipSync = value;
|
|
if ( Sound.IsValid() )
|
|
Sound.LipSync.Enabled = lipSync;
|
|
}
|
|
}
|
|
|
|
private float distance = 15_000f;
|
|
|
|
/// <inheritdoc cref="SoundHandle.Distance"/>
|
|
public float Distance
|
|
{
|
|
get => distance;
|
|
set
|
|
{
|
|
if ( value == distance )
|
|
return;
|
|
|
|
distance = value;
|
|
if ( Sound.IsValid() )
|
|
Sound.Distance = distance;
|
|
}
|
|
}
|
|
|
|
private Curve falloff = new( new( 0, 1, 0, -1.8f ), new( 0.05f, 0.22f, 3.5f, -3.5f ), new( 0.2f, 0.04f, 0.16f, -0.16f ), new( 1, 0 ) );
|
|
|
|
/// <inheritdoc cref="SoundHandle.Falloff"/>
|
|
public Curve Falloff
|
|
{
|
|
get => falloff;
|
|
set
|
|
{
|
|
falloff = value;
|
|
if ( Sound.IsValid() )
|
|
Sound.Falloff = falloff;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A list of 15 lipsync viseme weights. Requires <see cref="LipSync"/> to be enabled.
|
|
/// </summary>
|
|
public IReadOnlyList<float> Visemes => Sound.IsValid() ? Sound.LipSync.Visemes : Array.Empty<float>();
|
|
|
|
internal ReadOnlySpan<float> GetSpectrum()
|
|
{
|
|
CUtlVectorFloat spectrumVector = CUtlVectorFloat.Create( 0, 0 );
|
|
Player.native.GetSpectrum( spectrumVector );
|
|
|
|
var spectrum = new float[spectrumVector.Count()];
|
|
|
|
for ( var i = 0; i < spectrum.Length; ++i ) spectrum[i] = spectrumVector.Element( i );
|
|
|
|
spectrumVector.DeleteThis();
|
|
|
|
return spectrum;
|
|
}
|
|
|
|
internal float GetAmplitude()
|
|
{
|
|
return Player.native.GetAmplitude();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get meta data string.
|
|
/// </summary>
|
|
internal string GetMeta( string key )
|
|
{
|
|
return native.GetMetadata( key );
|
|
}
|
|
|
|
public VideoPlayer()
|
|
{
|
|
InteropSystem.AllocWeak( this );
|
|
native = CVideoPlayer.Create( this );
|
|
|
|
Texture = Texture.Create( 1, 1 )
|
|
.WithName( "video-placeholder" )
|
|
.WithData( new byte[4] { 0, 0, 0, 0 } )
|
|
.Finish();
|
|
|
|
Texture.IsLoaded = false;
|
|
Texture.ParentObject = this;
|
|
|
|
Audio = new AudioAccessor( this );
|
|
}
|
|
|
|
~VideoPlayer()
|
|
{
|
|
MainThread.QueueDispose( this );
|
|
}
|
|
|
|
internal void OnInitAudioInternal( int sampleRate, int channels )
|
|
{
|
|
SampleRate = sampleRate;
|
|
Channels = channels;
|
|
|
|
MainThread.Queue( OnAudioReadyInternal );
|
|
}
|
|
|
|
internal void OnFreeAudioInternal()
|
|
{
|
|
MainThread.Queue( FreeAudio );
|
|
}
|
|
|
|
private void FreeAudio()
|
|
{
|
|
Sound?.Dispose();
|
|
Sound = null;
|
|
}
|
|
|
|
internal void OnAudioReadyInternal()
|
|
{
|
|
FreeAudio();
|
|
|
|
var stream = native.GetAudioStream();
|
|
if ( stream is not null )
|
|
{
|
|
Sound = stream.Play();
|
|
if ( Sound is not null )
|
|
{
|
|
Sound.Position = Audio.Position;
|
|
Sound.ListenLocal = Audio.ListenLocal;
|
|
Sound.TargetMixer = Audio.TargetMixer;
|
|
Sound.Volume = Audio.Volume;
|
|
Sound.Distance = Audio.Distance;
|
|
Sound.Falloff = Audio.Falloff;
|
|
Sound.LipSync.Enabled = Audio.LipSync;
|
|
}
|
|
}
|
|
|
|
OnAudioReady?.Invoke();
|
|
}
|
|
|
|
internal void OnFinishedInternal()
|
|
{
|
|
MainThread.Queue( () =>
|
|
{
|
|
OnFinished?.Invoke();
|
|
} );
|
|
}
|
|
|
|
internal void OnRepeatedInternal()
|
|
{
|
|
MainThread.Queue( () =>
|
|
{
|
|
OnRepeated?.Invoke();
|
|
} );
|
|
}
|
|
|
|
internal void OnTextureCreatedInternal()
|
|
{
|
|
Texture.CopyFrom( Texture.FromNative( native.GetTexture() ) );
|
|
Texture.IsLoaded = true;
|
|
}
|
|
|
|
internal bool WantsTextureData() => OnTextureData != null;
|
|
|
|
internal unsafe void OnTextureDataInternal( IntPtr data, int width, int height )
|
|
{
|
|
var size = new Vector2( width, height );
|
|
var dataSpan = new ReadOnlySpan<byte>( data.ToPointer(), width * height * 4 );
|
|
|
|
OnTextureData?.Invoke( dataSpan, size );
|
|
}
|
|
|
|
internal void OnLoadedInternal()
|
|
{
|
|
MainThread.Queue( () =>
|
|
{
|
|
OnLoaded?.Invoke();
|
|
} );
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
FreeAudio();
|
|
|
|
Texture.ParentObject = null;
|
|
|
|
if ( native.IsValid )
|
|
{
|
|
native.Destroy();
|
|
native = IntPtr.Zero;
|
|
}
|
|
|
|
InteropSystem.FreeWeak( this );
|
|
GC.SuppressFinalize( this );
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Plays a video file from a URL. If there's already a video playing, it will stop.
|
|
/// </summary>
|
|
public void Play( string url )
|
|
{
|
|
if ( string.IsNullOrWhiteSpace( url ) )
|
|
return;
|
|
|
|
url = url.Trim();
|
|
|
|
if ( !Uri.TryCreate( url, UriKind.Absolute, out var uri ) )
|
|
return;
|
|
|
|
var ext = System.IO.Path.GetExtension( url ).ToLower();
|
|
|
|
if ( uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps )
|
|
{
|
|
Log.Warning( $"VideoPlayer: Url Scheme not allowed [{uri.Scheme}]" );
|
|
return;
|
|
}
|
|
|
|
native.Play( url, ext );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Plays a video file from a relative path. If there's already a video playing, it will stop.
|
|
/// </summary>
|
|
public void Play( BaseFileSystem filesystem, string path )
|
|
{
|
|
if ( string.IsNullOrWhiteSpace( path ) )
|
|
return;
|
|
|
|
// if it looks like a url, play it as a url
|
|
if ( Uri.TryCreate( path, UriKind.Absolute, out var uri ) && (uri.Scheme == Uri.UriSchemeHttps || uri.Scheme == Uri.UriSchemeHttp) )
|
|
{
|
|
Play( path );
|
|
return;
|
|
}
|
|
|
|
var ext = System.IO.Path.GetExtension( path ).ToLower();
|
|
|
|
if ( !filesystem.FileExists( path ) )
|
|
return;
|
|
|
|
path = filesystem.GetFullPath( path );
|
|
if ( string.IsNullOrWhiteSpace( path ) )
|
|
return;
|
|
|
|
native.Play( path, ext );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resumes video playback.
|
|
/// </summary>
|
|
public void Resume()
|
|
{
|
|
native.Resume();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops video playback.
|
|
/// </summary>
|
|
public void Stop()
|
|
{
|
|
native.Stop();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pauses video playback.
|
|
/// </summary>
|
|
public void Pause()
|
|
{
|
|
native.Pause();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Toggle video playback
|
|
/// </summary>
|
|
public void TogglePause()
|
|
{
|
|
if ( IsPaused ) native.Resume();
|
|
else native.Pause();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the playback position to a specified time in the video, given in seconds.
|
|
/// </summary>
|
|
public void Seek( float time )
|
|
{
|
|
native.Seek( time );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Present a video frame.
|
|
/// </summary>
|
|
public void Present()
|
|
{
|
|
native.Update();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The video is muted
|
|
/// </summary>
|
|
public bool Muted
|
|
{
|
|
get => native.IsMuted();
|
|
set => native.SetMuted( value );
|
|
}
|
|
|
|
internal void SetVideoOnly()
|
|
{
|
|
native.SetVideoOnly();
|
|
}
|
|
}
|