【m】插件上传

This commit is contained in:
2026-04-28 16:48:04 +08:00
parent 459db5ec01
commit 753878bdbb
631 changed files with 91583 additions and 11 deletions

View File

@@ -0,0 +1,5 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Unity.RenderStreaming.RuntimeTests")]
[assembly: InternalsVisibleTo("Unity.RenderStreaming.EditorTests")]
[assembly: InternalsVisibleTo("Unity.RenderStreaming.Editor")]

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,29 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bc8ba17751ad4107a50fa1415017b6b1, type: 3}
m_Name: RenderStreamingSettings
m_EditorClassIdentifier:
automaticStreaming: 1
signalingSettings:
id: 0
references:
version: 1
00000000:
type: {class: WebSocketSignalingSettings, ns: Unity.RenderStreaming, asm: Unity.RenderStreaming}
data:
m_url: ws://127.0.0.1:80
m_iceServers:
- m_urls:
- stun:stun.l.google.com:19302
m_username:
m_credentialType: 0
m_credential:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: eaaad242393318e4f85c45e69c8837f0
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,140 @@
using System;
using Unity.WebRTC;
using UnityEngine;
namespace Unity.RenderStreaming
{
/// <summary>
/// Represents information about an audio codec, including its MIME type, SDP format parameters, channel count, and sample rate.
/// </summary>
[Serializable]
public class AudioCodecInfo : IEquatable<AudioCodecInfo>
{
[SerializeField]
private string m_MimeType;
[SerializeField]
private string m_SdpFmtpLine;
[SerializeField]
private int m_ChannelCount;
[SerializeField]
private int m_SampleRate;
/// <summary>
/// Gets the name of the audio codec.
/// </summary>
public string name { get { return m_MimeType.GetCodecName(); } }
/// <summary>
/// Gets the MIME type of the audio codec.
/// </summary>
public string mimeType { get { return m_MimeType; } }
/// <summary>
/// Gets the number of audio channels.
/// </summary>
public int channelCount { get { return m_ChannelCount; } }
/// <summary>
/// Gets the sample rate of the audio.
/// </summary>
public int sampleRate { get { return m_SampleRate; } }
/// <summary>
/// Gets the SDP format parameters line.
/// </summary>
public string sdpFmtpLine { get { return m_SdpFmtpLine; } }
static internal AudioCodecInfo Create(RTCRtpCodecCapability caps)
{
return new AudioCodecInfo(caps);
}
/// <summary>
/// Determines whether the specified <see cref="AudioCodecInfo"/> is equal to the current <see cref="AudioCodecInfo"/>.
/// </summary>
/// <example>
/// <code>
/// <![CDATA[
/// Debug.Log(audioStreamReceiver1.codec.Equals(audioStreamReceiver2.codec));
/// ]]>
///</code>
/// </example>
/// <param name="other">The <see cref="AudioCodecInfo"/> to compare with the current <see cref="AudioCodecInfo"/>.</param>
/// <returns>true if the specified <see cref="AudioCodecInfo"/> is equal to the current <see cref="AudioCodecInfo"/>; otherwise, false.</returns>
public bool Equals(AudioCodecInfo other)
{
if (other == null)
return false;
return this.mimeType == other.mimeType
&& this.sdpFmtpLine == other.sdpFmtpLine
&& this.channelCount == other.channelCount
&& this.sampleRate == other.sampleRate;
}
/// <summary>
/// Determines whether the specified object is equal to the current <see cref="AudioCodecInfo"/>.
/// </summary>
/// <param name="obj">The object to compare with the current <see cref="AudioCodecInfo"/>.</param>
/// <returns>true if the specified object is equal to the current <see cref="AudioCodecInfo"/>; otherwise, false.</returns>
public override bool Equals(object obj)
{
return obj is AudioCodecInfo ? Equals((AudioCodecInfo)obj) : base.Equals(obj);
}
/// <summary>
/// Returns a hash code for the <see cref="AudioCodecInfo"/>.
/// </summary>
/// <returns>A hash code for the current <see cref="AudioCodecInfo"/>.</returns>
public override int GetHashCode()
{
return new { mimeType, sdpFmtpLine, channelCount, sampleRate }.GetHashCode();
}
/// <summary>
/// Determines whether two specified instances of <see cref="AudioCodecInfo"/> are equal.
/// </summary>
/// <param name="left">The first <see cref="AudioCodecInfo"/> to compare.</param>
/// <param name="right">The second <see cref="AudioCodecInfo"/> to compare.</param>
/// <returns>true if the two <see cref="AudioCodecInfo"/> instances are equal; otherwise, false.</returns>
public static bool operator ==(AudioCodecInfo left, AudioCodecInfo right)
{
if (ReferenceEquals(left, null))
{
return ReferenceEquals(left, null);
}
else
{
return left.Equals(right);
}
}
/// <summary>
/// Determines whether two specified instances of <see cref="AudioCodecInfo"/> are not equal.
/// </summary>
/// <param name="left">The first <see cref="AudioCodecInfo"/> to compare.</param>
/// <param name="right">The second <see cref="AudioCodecInfo"/> to compare.</param>
/// <returns>true if the two <see cref="AudioCodecInfo"/> instances are not equal; otherwise, false.</returns>
public static bool operator !=(AudioCodecInfo left, AudioCodecInfo right)
{
return !(left == right);
}
internal AudioCodecInfo(RTCRtpCodecCapability cap)
{
m_MimeType = cap.mimeType;
m_SdpFmtpLine = cap.sdpFmtpLine;
m_ChannelCount = cap.channels.GetValueOrDefault();
m_SampleRate = cap.clockRate.GetValueOrDefault();
}
internal bool Equals(RTCRtpCodecCapability other)
{
if (other == null)
return false;
return this.mimeType == other.mimeType
&& this.sdpFmtpLine == other.sdpFmtpLine
&& this.channelCount == other.channels
&& this.sampleRate == other.clockRate;
}
}
}

View File

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

View File

@@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Unity.WebRTC;
using UnityEngine;
namespace Unity.RenderStreaming
{
/// <summary>
/// AudioStreamReceiver is a component that receives audio streams and plays them through a specified AudioSource.
/// </summary>
/// <seealso cref="AudioCodecInfo"/>
[AddComponentMenu("Render Streaming/Audio Stream Receiver")]
public class AudioStreamReceiver : StreamReceiverBase
{
internal const string CodecPropertyName = nameof(m_Codec);
internal const string TargetAudioSourcePropertyName = nameof(m_TargetAudioSource);
/// <summary>
/// Delegate for handling updates to the received audio source.
/// </summary>
/// <param name="source">The updated AudioSource.</param>
public delegate void OnUpdateReceiveAudioSourceHandler(AudioSource source);
/// <summary>
/// Event triggered when the received audio source is updated.
/// </summary>
public OnUpdateReceiveAudioSourceHandler OnUpdateReceiveAudioSource;
[SerializeField]
private AudioSource m_TargetAudioSource;
[SerializeField, Codec]
private AudioCodecInfo m_Codec;
/// <summary>
/// Gets the codec information for the audio stream.
/// </summary>
public AudioCodecInfo codec
{
get { return m_Codec; }
}
/// <summary>
/// Gets or sets the target AudioSource where the received audio will be played.
/// </summary>
public AudioSource targetAudioSource
{
get { return m_TargetAudioSource; }
set { m_TargetAudioSource = value; }
}
/// <summary>
/// Gets the available audio codecs.
/// </summary>
/// <code>
/// var codecs = AudioStreamReceiver.GetAvailableCodecs();
/// foreach (var codec in codecs)
/// Debug.Log(codec.name);
/// </code>
/// </example>
/// <returns>A list of available codecs.</returns>
static public IEnumerable<AudioCodecInfo> GetAvailableCodecs()
{
var excludeCodecMimeType = new[] { "audio/CN", "audio/telephone-event" };
var capabilities = RTCRtpReceiver.GetCapabilities(TrackKind.Audio);
return capabilities.codecs.Where(codec => !excludeCodecMimeType.Contains(codec.mimeType)).Select(codec => AudioCodecInfo.Create(codec));
}
/// <summary>
/// Sets the codec for the audio stream.
/// </summary>
/// <example>
/// <code>
/// <![CDATA[
/// var codec = AudioStreamReceiver.GetAvailableCodecs().FirstOrDefault(x => x.mimeType.Contains("opus"));
/// audioStreamReceiver.SetCodec(codec);
/// ]]>
///</code>
/// </example>
/// <param name="codec">The codec information to set.</param>
/// <exception cref="InvalidOperationException">Thrown if the transceiver is streaming or the track has ended.</exception>
public void SetCodec(AudioCodecInfo codec)
{
m_Codec = codec;
if (Transceiver == null)
return;
if (!string.IsNullOrEmpty(Transceiver.Mid))
throw new InvalidOperationException("Transceiver is streaming. This operation is invalid during the track is in use.");
if (Transceiver.Sender.Track.ReadyState == TrackState.Ended)
throw new InvalidOperationException("Track has already been ended.");
var codecs = new AudioCodecInfo[] { m_Codec };
RTCErrorType error = Transceiver.SetCodecPreferences(SelectCodecCapabilities(codecs).ToArray());
if (error != RTCErrorType.None)
throw new InvalidOperationException($"Set codec is failed. errorCode={error}");
}
internal IEnumerable<RTCRtpCodecCapability> SelectCodecCapabilities(IEnumerable<AudioCodecInfo> codecs)
{
return RTCRtpReceiver.GetCapabilities(TrackKind.Audio).SelectCodecCapabilities(codecs);
}
private protected virtual void Start()
{
OnStartedStream += StartedStream;
OnStoppedStream += StoppedStream;
}
private void StartedStream(string connectionId)
{
if (Track is AudioStreamTrack audioTrack)
{
m_TargetAudioSource?.SetTrack(audioTrack);
OnUpdateReceiveAudioSource?.Invoke(m_TargetAudioSource);
}
}
private void StoppedStream(string connectionId)
{
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a6bfd564296404f5897c569b1a6b352b
timeCreated: 1600837296

View File

@@ -0,0 +1,568 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Unity.Collections;
using Unity.WebRTC;
using UnityEngine;
namespace Unity.RenderStreaming
{
/// <summary>
/// Specifies the source of the audio stream.
/// </summary>
public enum AudioStreamSource
{
/// <summary>
/// Use the AudioListener component as the audio source.
/// </summary>
AudioListener = 0,
/// <summary>
/// Use the AudioSource component as the audio source.
/// </summary>
AudioSource = 1,
/// <summary>
/// Use the microphone as the audio source.
/// </summary>
Microphone = 2,
/// <summary>
/// Use only the API to provide audio data.
/// </summary>
APIOnly = 3
}
/// <summary>
/// Component for sending audio streams.
/// </summary>
/// <seealso cref="AudioStreamSource"/>
/// <seealso cref="AudioCodecInfo"/>
[AddComponentMenu("Render Streaming/Audio Stream Sender")]
public class AudioStreamSender : StreamSenderBase
{
static readonly uint s_defaultMinBitrate = 0;
static readonly uint s_defaultMaxBitrate = 200;
internal const string SourcePropertyName = nameof(m_Source);
internal const string AudioSourcePropertyName = nameof(m_AudioSource);
internal const string AudioListenerPropertyName = nameof(m_AudioListener);
internal const string MicrophoneDeviceIndexPropertyName = nameof(m_MicrophoneDeviceIndex);
internal const string AutoRequestUserAuthorizationPropertyName = nameof(m_AutoRequestUserAuthorization);
internal const string CodecPropertyName = nameof(m_Codec);
internal const string BitratePropertyName = nameof(m_Bitrate);
internal const string LoopbackPropertyName = nameof(m_Loopback);
[SerializeField]
private AudioStreamSource m_Source;
[SerializeField]
private AudioListener m_AudioListener;
[SerializeField]
private AudioSource m_AudioSource;
[SerializeField]
private int m_MicrophoneDeviceIndex;
[SerializeField]
private bool m_AutoRequestUserAuthorization = true;
[SerializeField, Codec]
private AudioCodecInfo m_Codec;
[SerializeField, Bitrate(0, 1000)]
private Range m_Bitrate = new Range(s_defaultMinBitrate, s_defaultMaxBitrate);
[SerializeField]
private bool m_Loopback = false;
private int m_sampleRate = 0;
private AudioStreamSourceImpl m_sourceImpl = null;
private int m_frequency = 48000;
/// <summary>
/// Gets or sets the source of the audio stream.
/// </summary>
public AudioStreamSource source
{
get { return m_Source; }
set
{
if (m_Source == value)
return;
m_Source = value;
if (!isPlaying)
return;
var op = CreateTrack();
StartCoroutineWithCallback(op, _ => ReplaceTrack(_.Track));
}
}
/// <summary>
/// Gets the codec used for the audio stream.
/// </summary>
public AudioCodecInfo codec
{
get { return m_Codec; }
}
/// <summary>
/// Gets the minimum bitrate for the audio stream.
/// </summary>
public uint minBitrate
{
get { return m_Bitrate.min; }
}
/// <summary>
/// Gets the maximum bitrate for the audio stream.
/// </summary>
public uint maxBitrate
{
get { return m_Bitrate.max; }
}
/// <summary>
/// Gets or sets whether to play the audio locally while sending it to the remote peer.
/// </summary>
public bool loopback
{
get
{
return m_Loopback;
}
set
{
if (m_Loopback == value)
{
return;
}
m_Loopback = value;
if (Track is AudioStreamTrack audioTrack)
{
audioTrack.Loopback = value;
}
}
}
/// <summary>
/// Gets or sets the index of the microphone device used as the audio source.
/// </summary>
public int sourceDeviceIndex
{
get { return m_MicrophoneDeviceIndex; }
set
{
if (m_MicrophoneDeviceIndex == value)
return;
m_MicrophoneDeviceIndex = value;
if (!isPlaying || m_Source != AudioStreamSource.Microphone)
return;
var op = CreateTrack();
StartCoroutineWithCallback(op, _ => ReplaceTrack(_.Track));
}
}
/// <summary>
/// Gets or sets the AudioSource component used as the audio source.
/// </summary>
public AudioSource audioSource
{
get { return m_AudioSource; }
set
{
if (m_AudioSource == value)
return;
m_AudioSource = value;
if (!isPlaying || m_Source != AudioStreamSource.AudioSource)
return;
var op = CreateTrack();
StartCoroutineWithCallback(op, _ => ReplaceTrack(_.Track));
}
}
/// <summary>
/// Gets or sets the AudioListener component used as the audio source.
/// </summary>
public AudioListener audioListener
{
get { return m_AudioListener; }
set
{
if (m_AudioListener == value)
return;
m_AudioListener = value;
if (!isPlaying || m_Source != AudioStreamSource.AudioListener)
return;
var op = CreateTrack();
StartCoroutineWithCallback(op, _ => ReplaceTrack(_.Track));
}
}
/// <summary>
/// Gets the available video codecs.
/// </summary>
/// <code>
/// var codecs = VideoStreamSender.GetAvailableCodecs();
/// foreach (var codec in codecs)
/// Debug.Log(codec.name);
/// </code>
/// </example>
/// <returns>A list of available codecs.</returns>
static public IEnumerable<AudioCodecInfo> GetAvailableCodecs()
{
var excludeCodecMimeType = new[] { "audio/CN", "audio/telephone-event" };
var capabilities = RTCRtpSender.GetCapabilities(TrackKind.Audio);
return capabilities.codecs.Where(codec => !excludeCodecMimeType.Contains(codec.mimeType)).Select(codec => AudioCodecInfo.Create(codec));
}
/// <summary>
/// Sets the bitrate range for the audio stream.
/// </summary>
/// <example>
/// <code>
/// audioStreamSender.SetBitrate(128, 256);
/// </code>
/// </example>
/// <param name="minBitrate">The minimum bitrate in kbps. Must be greater than zero.</param>
/// <param name="maxBitrate">The maximum bitrate in kbps. Must be greater than or equal to the minimum bitrate.</param>
/// <exception cref="ArgumentException">Thrown when the maximum bitrate is less than the minimum bitrate.</exception>
public void SetBitrate(uint minBitrate, uint maxBitrate)
{
if (minBitrate > maxBitrate)
throw new ArgumentException("The maxBitrate must be greater than minBitrate.", "maxBitrate");
m_Bitrate.min = minBitrate;
m_Bitrate.max = maxBitrate;
foreach (var transceiver in Transceivers.Values)
{
RTCError error = transceiver.Sender.SetBitrate(m_Bitrate.min, m_Bitrate.max);
if (error.errorType != RTCErrorType.None)
RenderStreaming.Logger.Log(LogType.Error, error.message);
}
}
/// <summary>
/// Sets the codec for the audio stream.
/// </summary>
/// <example>
/// <code>
/// <![CDATA[
/// var codec = AudioStreamSender.GetAvailableCodecs().First(x => x.mimeType.Contains("opus"));
/// audioStreamSender.SetCodec(codec);
/// ]]>
///</code>
/// </example>
/// <param name="codec">The codec information to set.</param>
public void SetCodec(AudioCodecInfo codec)
{
m_Codec = codec;
foreach (var transceiver in Transceivers.Values)
{
if (!string.IsNullOrEmpty(transceiver.Mid))
continue;
if (transceiver.Sender.Track.ReadyState == TrackState.Ended)
continue;
var codecs = new AudioCodecInfo[] { m_Codec };
RTCErrorType error = transceiver.SetCodecPreferences(SelectCodecCapabilities(codecs).ToArray());
if (error != RTCErrorType.None)
throw new InvalidOperationException($"Set codec is failed. errorCode={error}");
}
}
internal IEnumerable<RTCRtpCodecCapability> SelectCodecCapabilities(IEnumerable<AudioCodecInfo> codecs)
{
return RTCRtpSender.GetCapabilities(TrackKind.Audio).SelectCodecCapabilities(codecs);
}
private protected virtual void Awake()
{
OnStartedStream += _OnStartedStream;
OnStoppedStream += _OnStoppedStream;
}
private protected override void OnDestroy()
{
base.OnDestroy();
m_sourceImpl?.Dispose();
m_sourceImpl = null;
}
void OnAudioConfigurationChanged(bool deviceWasChanged)
{
m_sampleRate = AudioSettings.outputSampleRate;
}
void _OnStartedStream(string connectionId)
{
}
void _OnStoppedStream(string connectionId)
{
m_sourceImpl?.Dispose();
m_sourceImpl = null;
}
internal override WaitForCreateTrack CreateTrack()
{
m_sourceImpl?.Dispose();
m_sourceImpl = CreateAudioStreamSource();
return m_sourceImpl.CreateTrack();
}
AudioStreamSourceImpl CreateAudioStreamSource()
{
switch (m_Source)
{
case AudioStreamSource.AudioListener:
return new AudioStreamSourceAudioListener(this);
case AudioStreamSource.AudioSource:
return new AudioStreamSourceAudioSource(this);
case AudioStreamSource.Microphone:
return new AudioStreamSourceMicrophone(this);
case AudioStreamSource.APIOnly:
return new AudioStreamSourceAPIOnly(this);
}
throw new InvalidOperationException("");
}
private protected override void OnEnable()
{
OnAudioConfigurationChanged(false);
AudioSettings.OnAudioConfigurationChanged += OnAudioConfigurationChanged;
base.OnEnable();
}
private protected override void OnDisable()
{
AudioSettings.OnAudioConfigurationChanged -= OnAudioConfigurationChanged;
base.OnDisable();
}
/// <summary>
/// Sets the audio data for the stream.
/// </summary>
/// <example>
/// <code>
/// <![CDATA[
/// int sampleRate = AudioSettings.outputSampleRate;
/// int frequency = 440;
/// int bufferSize = sampleRate; // 1 second buffer
/// var audioData = new NativeArray<float>(bufferSize, Allocator.Temp);
/// for (int i = 0; i < bufferSize; i++)
/// {
/// audioData[i] = Mathf.Sin(2 * Mathf.PI * frequency * i / sampleRate);
/// }
/// audioStreamSender.SetData(audioData.AsReadOnly(), 1);
/// audioData.Dispose();
/// ]]>
/// </code>
/// </example>
/// <param name="nativeArray">The native array containing the audio data.</param>
/// <param name="channels">The number of audio channels.</param>
/// <exception cref="InvalidOperationException">Thrown when the source property is not set to AudioStreamSource.APIOnly.</exception>
public void SetData(NativeArray<float>.ReadOnly nativeArray, int channels)
{
if (m_Source != AudioStreamSource.APIOnly)
throw new InvalidOperationException("To use this method, please set AudioStreamSource.APIOnly to source property");
if (!isPlaying)
return;
(m_sourceImpl as AudioStreamSourceAPIOnly)?.SetData(nativeArray, channels, m_sampleRate);
}
abstract class AudioStreamSourceImpl : IDisposable
{
protected AudioStreamSourceImpl(AudioStreamSender parent)
{
}
public abstract WaitForCreateTrack CreateTrack();
public abstract void Dispose();
}
class AudioStreamSourceAudioListener : AudioStreamSourceImpl
{
private AudioListener m_audioListener;
public AudioStreamSourceAudioListener(AudioStreamSender parent) : base(parent)
{
m_audioListener = parent.m_AudioListener;
if (m_audioListener == null)
throw new InvalidOperationException("The audioListener is not assigned.");
}
public override WaitForCreateTrack CreateTrack()
{
var instruction = new WaitForCreateTrack();
instruction.Done(new AudioStreamTrack(m_audioListener));
return instruction;
}
public override void Dispose()
{
GC.SuppressFinalize(this);
}
~AudioStreamSourceAudioListener()
{
Dispose();
}
}
class AudioStreamSourceAudioSource : AudioStreamSourceImpl
{
private AudioSource m_audioSource;
public AudioStreamSourceAudioSource(AudioStreamSender parent) : base(parent)
{
m_audioSource = parent.m_AudioSource;
if (m_audioSource == null)
throw new InvalidOperationException("The audioSource is not assigned.");
}
public override WaitForCreateTrack CreateTrack()
{
var instruction = new WaitForCreateTrack();
instruction.Done(new AudioStreamTrack(m_audioSource));
return instruction;
}
public override void Dispose()
{
GC.SuppressFinalize(this);
}
~AudioStreamSourceAudioSource()
{
Dispose();
}
}
class AudioStreamSourceMicrophone : AudioStreamSourceImpl
{
int m_deviceIndex;
bool m_autoRequestUserAuthorization;
int m_frequency;
string m_deviceName;
AudioSource m_audioSource;
GameObject m_audioSourceObj;
AudioStreamSender m_parent;
public AudioStreamSourceMicrophone(AudioStreamSender parent) : base(parent)
{
int deviceIndex = parent.m_MicrophoneDeviceIndex;
if (deviceIndex < 0 || Microphone.devices.Length <= deviceIndex)
throw new ArgumentOutOfRangeException("deviceIndex", deviceIndex, "The deviceIndex is out of range");
m_parent = parent;
m_deviceIndex = deviceIndex;
m_frequency = parent.m_frequency;
m_autoRequestUserAuthorization = parent.m_AutoRequestUserAuthorization;
}
public override WaitForCreateTrack CreateTrack()
{
var instruction = new WaitForCreateTrack();
m_parent.StartCoroutine(CreateTrackCoroutine(instruction));
return instruction;
}
IEnumerator CreateTrackCoroutine(WaitForCreateTrack instruction)
{
if (m_autoRequestUserAuthorization)
{
AsyncOperation op = Application.RequestUserAuthorization(UserAuthorization.Microphone);
yield return op;
}
if (!Application.HasUserAuthorization(UserAuthorization.Microphone))
throw new InvalidOperationException("Call Application.RequestUserAuthorization before creating track with Microphone.");
m_deviceName = Microphone.devices[m_deviceIndex];
Microphone.GetDeviceCaps(m_deviceName, out int minFreq, out int maxFreq);
var micClip = Microphone.Start(m_deviceName, true, 1, m_frequency);
// set the latency to “0” samples before the audio starts to play.
yield return new WaitUntil(() => Microphone.GetPosition(m_deviceName) > 0);
m_audioSourceObj = new GameObject("Audio");
m_audioSourceObj.hideFlags = HideFlags.HideInHierarchy;
DontDestroyOnLoad(m_audioSourceObj);
m_audioSource = m_audioSourceObj.AddComponent<AudioSource>();
m_audioSource.clip = micClip;
m_audioSource.loop = true;
m_audioSource.Play();
instruction.Done(new AudioStreamTrack(m_audioSource));
}
public override void Dispose()
{
if (m_audioSourceObj != null)
{
m_audioSource.Stop();
var clip = m_audioSource.clip;
if (clip != null)
{
Destroy(clip);
}
m_audioSource.clip = null;
Destroy(m_audioSourceObj);
m_audioSourceObj = null;
m_audioSource = null;
}
if (Microphone.IsRecording(m_deviceName))
Microphone.End(m_deviceName);
GC.SuppressFinalize(this);
}
~AudioStreamSourceMicrophone()
{
Dispose();
}
}
class AudioStreamSourceAPIOnly : AudioStreamSourceImpl
{
AudioStreamTrack m_audioTrack;
public AudioStreamSourceAPIOnly(AudioStreamSender parent) : base(parent)
{
}
public override WaitForCreateTrack CreateTrack()
{
var instruction = new WaitForCreateTrack();
m_audioTrack = new AudioStreamTrack();
instruction.Done(m_audioTrack);
return instruction;
}
public void SetData(NativeArray<float>.ReadOnly nativeArray, int channels, int sampleRate)
{
m_audioTrack?.SetData(nativeArray, channels, sampleRate);
}
public override void Dispose()
{
GC.SuppressFinalize(this);
}
~AudioStreamSourceAPIOnly()
{
Dispose();
}
}
}
}

View File

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

View File

@@ -0,0 +1,180 @@
using System;
using System.Linq;
using Unity.Collections;
using Unity.RenderStreaming.InputSystem;
using Unity.WebRTC;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Users;
using UnityEngine.SceneManagement;
namespace Unity.RenderStreaming
{
internal class AutomaticStreaming : MonoBehaviour
{
private SignalingManager renderstreaming;
private Broadcast broadcast;
private VideoStreamSender videoStreamSender;
private AudioStreamSender audioStreamSender;
private AutoInputReceiver inputReceiver;
private void Awake()
{
gameObject.hideFlags = HideFlags.HideInHierarchy;
broadcast = gameObject.AddComponent<Broadcast>();
videoStreamSender = gameObject.AddComponent<VideoStreamSender>();
videoStreamSender.source = VideoStreamSource.Screen;
videoStreamSender.SetTextureSize(new Vector2Int(Screen.width, Screen.height));
broadcast.AddComponent(videoStreamSender);
audioStreamSender = gameObject.AddComponent<AudioStreamSender>();
audioStreamSender.source = AudioStreamSource.APIOnly;
broadcast.AddComponent(audioStreamSender);
inputReceiver = gameObject.AddComponent<AutoInputReceiver>();
broadcast.AddComponent(inputReceiver);
renderstreaming = gameObject.AddComponent<SignalingManager>();
renderstreaming.AddSignalingHandler(broadcast);
renderstreaming.Run();
SceneManager.activeSceneChanged += (scene1, scene2) =>
{
var audioListener = FindObjectOfType<AudioListener>();
if (audioListener == null || audioListener.gameObject.GetComponent<AutoAudioFilter>() != null)
{
return;
}
var autoFilter = audioListener.gameObject.AddComponent<AutoAudioFilter>();
autoFilter.SetSender(audioStreamSender);
};
}
private void OnDestroy()
{
renderstreaming.Stop();
renderstreaming = null;
broadcast = null;
videoStreamSender = null;
audioStreamSender = null;
inputReceiver = null;
}
class AutoAudioFilter : MonoBehaviour
{
private AudioStreamSender sender;
public void SetSender(AudioStreamSender sender)
{
this.sender = sender;
}
private void Awake()
{
this.hideFlags = HideFlags.HideInInspector;
}
private void OnAudioFilterRead(float[] data, int channels)
{
if (sender == null || sender.source != AudioStreamSource.APIOnly)
{
return;
}
var nativeArray = new NativeArray<float>(data, Allocator.Temp);
sender.SetData(nativeArray.AsReadOnly(), channels);
nativeArray.Dispose();
}
private void OnDestroy()
{
sender = null;
}
}
class AutoInputReceiver : InputChannelReceiverBase
{
public override event Action<InputDevice, InputDeviceChange> onDeviceChange;
protected virtual void OnEnable()
{
onDeviceChange += OnDeviceChange;
}
protected virtual void OnDisable()
{
onDeviceChange -= OnDeviceChange;
}
private void PerformPairingWithDevice(InputDevice device)
{
inputUser = InputUser.PerformPairingWithDevice(device, inputUser);
}
private void UnpairDevices(InputDevice device)
{
if (!inputUser.valid)
return;
inputUser.UnpairDevice(device);
}
public override void SetChannel(string connectionId, RTCDataChannel channel)
{
if (channel == null)
{
Dispose();
}
else
{
AssignUserAndDevices();
receiver = new Receiver(channel);
receiver.onDeviceChange += onDeviceChange;
receiverInput = new InputSystem.InputRemoting(receiver);
subscriberDisposer = receiverInput.Subscribe(receiverInput);
receiverInput.StartSending();
}
base.SetChannel(connectionId, channel);
}
protected virtual void OnDestroy()
{
Dispose();
}
protected virtual void Dispose()
{
receiverInput?.StopSending();
subscriberDisposer?.Dispose();
receiver?.Dispose();
receiver = null;
}
[NonSerialized] private InputUser inputUser;
[NonSerialized] private Receiver receiver;
[NonSerialized] private InputSystem.InputRemoting receiverInput;
[NonSerialized] private IDisposable subscriberDisposer;
private void AssignUserAndDevices()
{
inputUser = InputUser.all.FirstOrDefault();
}
protected virtual void OnDeviceChange(InputDevice device, InputDeviceChange change)
{
switch (change)
{
case InputDeviceChange.Added:
PerformPairingWithDevice(device);
return;
case InputDeviceChange.Removed:
UnpairDevices(device);
return;
}
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 50f50ea0132f471fa447969dcbceb3e9
timeCreated: 1674179059

View File

@@ -0,0 +1,100 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace Unity.RenderStreaming
{
public class Broadcast : SignalingHandlerBase,
IOfferHandler, IAddChannelHandler, IDisconnectHandler, IDeletedConnectionHandler,
IAddReceiverHandler
{
[SerializeField] private List<Component> streams = new List<Component>();
private List<string> connectionIds = new List<string>();
public override IEnumerable<Component> Streams => streams;
public void AddComponent(Component component)
{
streams.Add(component);
}
public void RemoveComponent(Component component)
{
streams.Remove(component);
}
public void OnDeletedConnection(SignalingEventData eventData)
{
Disconnect(eventData.connectionId);
}
public void OnDisconnect(SignalingEventData eventData)
{
Disconnect(eventData.connectionId);
}
private void Disconnect(string connectionId)
{
if (!connectionIds.Contains(connectionId))
return;
connectionIds.Remove(connectionId);
foreach (var sender in streams.OfType<IStreamSender>())
{
RemoveSender(connectionId, sender);
}
foreach (var receiver in streams.OfType<IStreamReceiver>())
{
RemoveReceiver(connectionId, receiver);
}
foreach (var channel in streams.OfType<IDataChannel>().Where(c => c.ConnectionId == connectionId))
{
RemoveChannel(connectionId, channel);
}
}
public void OnAddReceiver(SignalingEventData data)
{
var track = data.transceiver.Receiver.Track;
IStreamReceiver receiver = GetReceiver(track.Kind);
SetReceiver(data.connectionId, receiver, data.transceiver);
}
public void OnOffer(SignalingEventData data)
{
if (connectionIds.Contains(data.connectionId))
{
RenderStreaming.Logger.Log($"Already answered this connectionId : {data.connectionId}");
return;
}
connectionIds.Add(data.connectionId);
foreach (var source in streams.OfType<IStreamSender>())
{
AddSender(data.connectionId, source);
}
foreach (var channel in streams.OfType<IDataChannel>().Where(c => c.IsLocal))
{
AddChannel(data.connectionId, channel);
}
SendAnswer(data.connectionId);
}
public void OnAddChannel(SignalingEventData data)
{
var channel = streams.OfType<IDataChannel>().
FirstOrDefault(r => !r.IsConnected && !r.IsLocal);
channel?.SetChannel(data.connectionId, data.channel);
}
IStreamReceiver GetReceiver(WebRTC.TrackKind kind)
{
if (kind == WebRTC.TrackKind.Audio)
return streams.OfType<AudioStreamReceiver>().First();
if (kind == WebRTC.TrackKind.Video)
return streams.OfType<VideoStreamReceiver>().First();
throw new System.ArgumentException();
}
}
}

View File

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

View File

@@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Linq;
using Unity.WebRTC;
namespace Unity.RenderStreaming
{
internal static class RTCRtpCodecCapabilityExtension
{
public static string GetCodecName(this string mimeType)
{
if (mimeType == null)
return null;
string[] substrings = mimeType.Split('/');
if (substrings.Length > 1)
return substrings[1];
return null;
}
public static IEnumerable<RTCRtpCodecCapability> SelectCodecCapabilities(this RTCRtpCapabilities capabilities, IEnumerable<VideoCodecInfo> codecs)
{
var caps = capabilities.codecs;
return codecs
.Where(codec => codec != null)
.Select(codec => caps.FirstOrDefault(cap => codec.Equals(cap)))
.Where(cap => cap != null);
}
public static IEnumerable<RTCRtpCodecCapability> SelectCodecCapabilities(this RTCRtpCapabilities capabilities, IEnumerable<AudioCodecInfo> codecs)
{
var caps = capabilities.codecs;
return codecs
.Where(codec => codec != null)
.Select(codec => caps.FirstOrDefault(cap => codec.Equals(cap)))
.Where(cap => cap != null);
}
}
}

View File

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

View File

@@ -0,0 +1,280 @@
using System;
using System.Collections.Generic;
using System.IO;
using Unity.RenderStreaming.Signaling;
using Unity.WebRTC;
using UnityEngine;
namespace Unity.RenderStreaming
{
[Serializable]
internal struct CommandLineInfo
{
public string signalingType;
public string signalingUrl;
public RTCIceServer[] iceServers;
public string pollingInterval;
}
static class CommandLineParser
{
internal static readonly StringArgument SignalingUrl = new StringArgument("-signalingUrl");
internal static readonly StringArgument SignalingType = new StringArgument("-signalingType");
internal static readonly StringArrayArgument IceServerUrls = new StringArrayArgument("-iceServerUrl");
internal static readonly StringArgument IceServerUsername = new StringArgument("-iceServerUsername");
internal static readonly StringArgument IceServerCredential = new StringArgument("-iceServerCredential");
internal static readonly EnumArgument<IceCredentialType> IceServerCredentialType = new EnumArgument<IceCredentialType>("-iceServerCredentialType");
internal static readonly IntArgument PollingInterval = new IntArgument("-pollingInterval");
internal static readonly JsonFileArgument<CommandLineInfo> ImportJson = new JsonFileArgument<CommandLineInfo>("-importJson");
static readonly List<IArgument> options = new List<IArgument>() { SignalingUrl, SignalingType, IceServerUrls, IceServerUsername, IceServerCredential, IceServerCredentialType, PollingInterval, ImportJson };
internal delegate bool TryParseDelegate<T>(string[] arguments, string argumentName, out T result);
internal interface IArgument
{
bool TryParse(string[] arguments);
}
internal abstract class BaseArgument<T> : IArgument
{
/// <summary>
/// A switch that will either retrieve the argument using the resolver, or use the readonly
/// argument name string set on construction.
/// </summary>
public string ArgumentName { get; }
/// <summary>
///
/// </summary>
public bool Defined => m_defined;
/// <summary>
///
/// </summary>
public readonly bool Required;
/// <summary>
///
/// </summary>
public T Value => m_value;
protected bool m_defined;
protected T m_value;
readonly TryParseDelegate<T> m_parser;
protected abstract bool DefaultParser(string[] arguments, string argumentName, out T parsedResult);
public bool TryParse(string[] arguments)
{
m_defined =
m_parser != null &&
m_parser(arguments, ArgumentName, out m_value);
return m_defined;
}
internal BaseArgument(string argumentName, bool required = false)
{
Required = required;
ArgumentName = argumentName;
m_parser = DefaultParser;
}
internal BaseArgument(string argumentName, TryParseDelegate<T> tryParseDelegate, bool required = false)
{
Required = required;
ArgumentName = argumentName;
m_parser = tryParseDelegate;
}
}
internal class StringArgument : BaseArgument<string>
{
protected override bool DefaultParser(string[] arguments, string argumentName, out string parsedResult) => TryParseStringArgument(arguments, argumentName, out parsedResult, Required);
internal StringArgument(string argumentName, bool required = false) : base(argumentName, required) { }
internal StringArgument(string argumentName, TryParseDelegate<string> tryParse, bool required = false) : base(argumentName, tryParse, required) { }
public static implicit operator string(StringArgument argument) => !argument.Defined ? null : argument.Value;
}
internal class EnumArgument<T> : BaseArgument<T?> where T : struct
{
protected override bool DefaultParser(string[] arguments, string argumentName, out T? parsedResult) => TryParseEnumArgument(arguments, argumentName, out parsedResult, Required);
internal EnumArgument(string argumentName, bool required = false) : base(argumentName, required) { }
public static implicit operator T?(EnumArgument<T> argument) => !argument.Defined ? null : argument.Value;
}
internal class StringArrayArgument : BaseArgument<string[]>
{
protected override bool DefaultParser(string[] arguments, string argumentName, out string[] parsedResult) => TryParseStringArrayArgument(arguments, argumentName, out parsedResult, Required);
internal StringArrayArgument(string argumentName, bool required = false) : base(argumentName, required) { }
public static implicit operator string[](StringArrayArgument argument) => !argument.Defined ? null : argument.Value;
}
internal class IntArgument : BaseArgument<int?>
{
protected override bool DefaultParser(string[] arguments, string argumentName, out int? parsedResult) => TryParseIntArgument(arguments, argumentName, out parsedResult, Required);
internal IntArgument(string argumentName, bool required = false) : base(argumentName, required) { }
public static implicit operator int?(IntArgument argument) => !argument.Defined ? null : argument.Value;
}
internal class JsonFileArgument<T> : BaseArgument<T?> where T : struct
{
protected override bool DefaultParser(string[] arguments, string argumentName, out T? parsedResult) => TryParseJsonFileArgument(arguments, argumentName, out parsedResult, Required);
internal JsonFileArgument(string argumentName, bool required = false) : base(argumentName, required) { }
public static implicit operator T?(JsonFileArgument<T> argument) => !argument.Defined ? null : argument.Value;
}
static bool TryParseStringArgument(string[] arguments, string argumentName, out string argumentValue, bool required = false)
{
var startIndex = System.Array.FindIndex(arguments, x => x == argumentName);
if (startIndex < 0)
{
argumentValue = null;
return !required;
}
if (startIndex + 1 >= arguments.Length)
{
argumentValue = null;
return false;
}
argumentValue = arguments[startIndex + 1];
return !string.IsNullOrEmpty(argumentValue);
}
static bool TryParseEnumArgument<T>(string[] arguments, string argumentName, out T? argumentValue,
bool required = false) where T : struct
{
bool result = TryParseStringArgument(arguments, argumentName, out string value, required);
if (result && !string.IsNullOrEmpty(value))
{
if (Enum.TryParse(value, true, out T enumValue))
{
argumentValue = enumValue;
return true;
}
result = false;
}
argumentValue = null;
return result;
}
static bool TryParseIntArgument(string[] arguments, string argumentName, out int? argumentValue, bool required = false)
{
var startIndex = System.Array.FindIndex(arguments, x => x == argumentName);
if (startIndex < 0)
{
argumentValue = null;
return !required;
}
if (startIndex + 1 >= arguments.Length)
{
argumentValue = null;
return false;
}
if (!int.TryParse(arguments[startIndex + 1], out var result))
{
argumentValue = null;
return false;
}
argumentValue = result;
return true;
}
static bool TryParseStringArrayArgument(string[] arguments, string argumentName, out string[] argumentValue, bool required = false)
{
List<string> list = new List<string>();
for (int i = 0; i < arguments.Length;)
{
var startIndex = Array.FindIndex(arguments, i, x => x == argumentName);
if (startIndex < 0)
break;
if (startIndex + 1 >= arguments.Length)
break;
list.Add(arguments[startIndex + 1]);
i = startIndex + 2;
}
if (list.Count == 0)
{
argumentValue = null;
return !required;
}
argumentValue = list.ToArray();
return true;
}
static bool TryParseJsonFileArgument<T>(string[] arguments, string argumentName, out T? argumentValue,
bool required = false) where T : struct
{
bool result = TryParseFilePathArgument(arguments, argumentName, out string value);
if (result && !string.IsNullOrEmpty(value))
{
string text = File.ReadAllText(value);
try
{
argumentValue = JsonUtility.FromJson<T>(text);
return true;
}
catch (Exception)
{
result = false;
}
}
else if (required)
result = false;
argumentValue = null;
return result;
}
static bool TryParseFilePathArgument(string[] arguments, string argumentName, out string argumentValue)
{
bool ret = TryParseStringArgument(arguments, argumentName, out string value);
if (!ret)
{
argumentValue = null;
return false;
}
if (string.IsNullOrEmpty(value))
{
argumentValue = null;
return true;
}
if (!File.Exists(value))
{
argumentValue = null;
return false;
}
argumentValue = value;
return true;
}
internal static bool TryParse(string[] arguments)
{
foreach (var option in options)
{
if (!option.TryParse(arguments))
return false;
}
return true;
}
}
}

View File

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

View File

@@ -0,0 +1,140 @@
using System;
using Unity.WebRTC;
using UnityEngine;
namespace Unity.RenderStreaming
{
/// <summary>
///
/// </summary>
public abstract class DataChannelBase : MonoBehaviour, IDataChannel
{
internal const string LocalPropertyName = nameof(local);
internal const string LabelPropertyName = nameof(label);
/// <summary>
///
/// </summary>
[SerializeField]
protected bool local = false;
/// <summary>
///
/// </summary>
[SerializeField]
protected string label;
/// <summary>
///
/// </summary>
public bool IsLocal => local;
/// <summary>
///
/// </summary>
public string Label => label;
/// <summary>
///
/// </summary>
public bool IsConnected => Channel != null && Channel.ReadyState == RTCDataChannelState.Open;
/// <summary>
///
/// </summary>
public string ConnectionId { get; protected set; }
/// <summary>
///
/// </summary>
public RTCDataChannel Channel { get; protected set; }
/// <summary>
///
/// </summary>
public OnStartedChannelHandler OnStartedChannel { get; set; }
/// <summary>
///
/// </summary>
public OnStoppedChannelHandler OnStoppedChannel { get; set; }
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <param name="channel"></param>
public virtual void SetChannel(string connectionId, RTCDataChannel channel)
{
Channel = channel;
if (Channel == null)
{
ConnectionId = String.Empty;
OnStoppedChannel?.Invoke(connectionId);
return;
}
ConnectionId = connectionId;
label = Channel.Label;
Channel.OnOpen += () => { OnOpen(connectionId); };
Channel.OnClose += () => { OnClose(connectionId); };
Channel.OnMessage += OnMessage;
if (Channel.ReadyState == RTCDataChannelState.Open && !IsLocal)
{
OnStartedChannel?.Invoke(connectionId);
}
}
/// <summary>
///
/// </summary>
/// <param name="msg"></param>
public virtual void Send(byte[] msg)
{
Channel.Send(msg);
}
/// <summary>
///
/// </summary>
/// <param name="msg"></param>
public virtual void Send(string msg)
{
Channel.Send(msg);
}
/// <summary>
///
/// </summary>
/// <param name="data"></param>
public virtual void SetChannel(SignalingEventData data)
{
SetChannel(data.connectionId, data.channel);
}
/// <summary>
///
/// </summary>
/// <param name="bytes"></param>
protected virtual void OnMessage(byte[] bytes)
{
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
protected virtual void OnOpen(string connectionId)
{
OnStartedChannel?.Invoke(connectionId);
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
protected virtual void OnClose(string connectionId)
{
OnStoppedChannel?.Invoke(connectionId);
}
}
}

View File

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

View File

@@ -0,0 +1,20 @@
using System;
namespace Unity.RenderStreaming
{
static class DateTimeExtension
{
private static readonly long DatetimeMinTimeTicks =
(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).Ticks;
/// <summary>
/// It returns Javascript format timestamp
/// </summary>
/// <param name="dt"></param>
/// <returns></returns>
public static long ToJsMilliseconds(this DateTime dt)
{
return (long)((dt.ToUniversalTime().Ticks - DatetimeMinTimeTicks) / 10000);
}
}
}

View File

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

View File

@@ -0,0 +1,102 @@
using System;
using UnityEngine.EventSystems;
namespace Unity.RenderStreaming
{
static class ExecuteSignalingEvents
{
public static T ValidateEventData<T>(BaseEventData data) where T : class
{
if ((data as T) == null)
throw new ArgumentException(
$"Invalid type: {data.GetType()} passed to event expecting {typeof(T)}");
return data as T;
}
private static readonly ExecuteEvents.EventFunction<ICreatedConnectionHandler>
s_CreatedConnectionHandler = Execute;
private static readonly ExecuteEvents.EventFunction<IDeletedConnectionHandler>
s_DeletedConnectionHandler = Execute;
private static readonly ExecuteEvents.EventFunction<IConnectHandler>
s_ConnectHandler = Execute;
private static readonly ExecuteEvents.EventFunction<IDisconnectHandler>
s_DisconnectHandler = Execute;
private static readonly ExecuteEvents.EventFunction<IOfferHandler>
s_OfferHandler = Execute;
private static readonly ExecuteEvents.EventFunction<IAnswerHandler>
s_AnswerHandler = Execute;
private static readonly ExecuteEvents.EventFunction<IAddChannelHandler>
s_AddChannelHandler = Execute;
private static readonly ExecuteEvents.EventFunction<IAddReceiverHandler>
s_AddReceiverHandler = Execute;
private static void Execute(ICreatedConnectionHandler handler, BaseEventData eventData)
{
handler.OnCreatedConnection(ValidateEventData<SignalingEventData>(eventData));
}
private static void Execute(IDeletedConnectionHandler handler, BaseEventData eventData)
{
handler.OnDeletedConnection(ValidateEventData<SignalingEventData>(eventData));
}
private static void Execute(IConnectHandler handler, BaseEventData eventData)
{
handler.OnConnect(ValidateEventData<SignalingEventData>(eventData));
}
private static void Execute(IDisconnectHandler handler, BaseEventData eventData)
{
handler.OnDisconnect(ValidateEventData<SignalingEventData>(eventData));
}
private static void Execute(IOfferHandler handler, BaseEventData eventData)
{
handler.OnOffer(ValidateEventData<SignalingEventData>(eventData));
}
private static void Execute(IAnswerHandler handler, BaseEventData eventData)
{
handler.OnAnswer(ValidateEventData<SignalingEventData>(eventData));
}
private static void Execute(IAddChannelHandler handler, BaseEventData eventData)
{
handler.OnAddChannel(ValidateEventData<SignalingEventData>(eventData));
}
private static void Execute(IAddReceiverHandler handler, BaseEventData eventData)
{
handler.OnAddReceiver(ValidateEventData<SignalingEventData>(eventData));
}
public static ExecuteEvents.EventFunction<ICreatedConnectionHandler> createdConnectionHandler
{
get { return s_CreatedConnectionHandler; }
}
public static ExecuteEvents.EventFunction<IDeletedConnectionHandler> deletedConnectionHandler
{
get { return s_DeletedConnectionHandler; }
}
public static ExecuteEvents.EventFunction<IConnectHandler> connectHandler
{
get { return s_ConnectHandler; }
}
public static ExecuteEvents.EventFunction<IDisconnectHandler> disconnectHandler
{
get { return s_DisconnectHandler; }
}
public static ExecuteEvents.EventFunction<IOfferHandler> offerHandler
{
get { return s_OfferHandler; }
}
public static ExecuteEvents.EventFunction<IAnswerHandler> answerHandler
{
get { return s_AnswerHandler; }
}
public static ExecuteEvents.EventFunction<IAddChannelHandler> addChannelHandler
{
get { return s_AddChannelHandler; }
}
public static ExecuteEvents.EventFunction<IAddReceiverHandler> addReceiverHandler
{
get { return s_AddReceiverHandler; }
}
}
}

View File

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

View File

@@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using Unity.WebRTC;
namespace Unity.RenderStreaming
{
public interface IRenderStreamingDelegate
{
/// <summary>
///
/// </summary>
event Action onStart;
/// <summary>
///
/// </summary>
event Action<string> onCreatedConnection;
/// <summary>
///
/// </summary>
event Action<string> onDeletedConnection;
/// <summary>
///
/// </summary>
event Action<string, string> onGotOffer;
/// <summary>
///
/// </summary>
event Action<string, string> onGotAnswer;
/// <summary>
///
/// </summary>
event Action<string> onConnect;
/// <summary>
///
/// </summary>
event Action<string> onDisconnect;
/// <summary>
///
/// </summary>
event Action<string, RTCRtpTransceiver> onAddTransceiver;
/// <summary>
///
/// </summary>
event Action<string, RTCDataChannel> onAddChannel;
}
public interface IRenderStreamingHandler
{
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
void CreateConnection(string connectionId);
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
void DeleteConnection(string connectionId);
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <returns></returns>
bool ExistConnection(string connectionId);
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <returns></returns>
bool IsConnected(string connectionId);
/// <summary>
///
/// </summary>
/// <param name="connection"></param>
/// <returns></returns>
bool IsStable(string connection);
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <param name="label"></param>
/// <returns></returns>
RTCDataChannel CreateChannel(string connectionId, string label = null);
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
void SendOffer(string connectionId);
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
void SendAnswer(string connectionId);
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <param name="track"></param>
/// <param name="init"></param>
/// <returns></returns>
RTCRtpTransceiver AddTransceiver(string connectionId, MediaStreamTrack track, RTCRtpTransceiverInit init = null);
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <param name="kind"></param>
/// <param name="init"></param>
/// <returns></returns>
RTCRtpTransceiver AddTransceiver(string connectionId, TrackKind kind, RTCRtpTransceiverInit init = null);
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <param name="track"></param>
void RemoveSenderTrack(string connectionId, MediaStreamTrack track);
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <returns></returns>
IEnumerable<RTCRtpTransceiver> GetTransceivers(string connectionId);
}
}

View File

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

View File

@@ -0,0 +1,44 @@
using UnityEngine.EventSystems;
namespace Unity.RenderStreaming
{
public interface ICreatedConnectionHandler : IEventSystemHandler
{
void OnCreatedConnection(SignalingEventData eventData);
}
public interface IDeletedConnectionHandler : IEventSystemHandler
{
void OnDeletedConnection(SignalingEventData eventData);
}
public interface IConnectHandler : IEventSystemHandler
{
void OnConnect(SignalingEventData eventData);
}
public interface IDisconnectHandler : IEventSystemHandler
{
void OnDisconnect(SignalingEventData eventData);
}
public interface IOfferHandler : IEventSystemHandler
{
void OnOffer(SignalingEventData eventData);
}
public interface IAnswerHandler : IEventSystemHandler
{
void OnAnswer(SignalingEventData eventData);
}
public interface IAddChannelHandler : IEventSystemHandler
{
void OnAddChannel(SignalingEventData eventData);
}
public interface IAddReceiverHandler : IEventSystemHandler
{
void OnAddReceiver(SignalingEventData eventData);
}
}

View File

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

View File

@@ -0,0 +1,18 @@
using System;
using UnityEngine.InputSystem;
namespace Unity.RenderStreaming
{
/// <summary>
///
/// </summary>
public abstract class InputChannelReceiverBase : DataChannelBase
{
/// <summary>
///
/// </summary>
#pragma warning disable 0067
public virtual event Action<InputDevice, InputDeviceChange> onDeviceChange;
#pragma warning restore 0067
}
}

View File

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

View File

@@ -0,0 +1,60 @@
using System;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.LowLevel;
namespace Unity.RenderStreaming
{
class InputPositionCorrector
{
public Rect inputRegion { set; get; }
public Rect outputRegion { set; get; }
private Action<InputEventPtr, InputDevice> _onEvent;
public InputPositionCorrector(Action<InputEventPtr, InputDevice> onEvent)
{
_onEvent = onEvent;
}
public unsafe void Invoke(InputEventPtr ptr, InputDevice device)
{
// Allocate memory and copy InputEventPtr
InputEventPtr dst = (InputEventPtr)
UnsafeUtility.Malloc(ptr.sizeInBytes, 4, Collections.Allocator.Temp);
UnsafeUtility.MemCpy(dst, ptr, ptr.sizeInBytes);
// Mapping
PointerMap((StateEvent*)dst.data, device);
_onEvent?.Invoke(dst, device);
// Free memory
UnsafeUtility.Free(dst, Collections.Allocator.Temp);
}
unsafe void PointerMap(StateEvent* data, InputDevice device)
{
switch (device)
{
case Mouse mouse:
MouseState* mouseState = (MouseState*)data->state;
mouseState->position = Map(mouseState->position, inputRegion, outputRegion);
break;
case Touchscreen touch:
// todo(kazuki): multi touch is not supported yet.
TouchState* touchState = (TouchState*)data->state;
touchState->position = Map(touchState->position, inputRegion, outputRegion);
break;
}
}
static Vector2 Map(Vector2 pos, Rect inputRegion, Rect outputRegion)
{
Vector2 normalized = Rect.PointToNormalized(inputRegion, pos);
return Rect.NormalizedToPoint(outputRegion, normalized);
}
}
}

View File

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

View File

@@ -0,0 +1,507 @@
using System;
using System.Linq;
using Unity.RenderStreaming.InputSystem;
using Unity.WebRTC;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Users;
using UnityEngine.InputSystem.Utilities;
using InputRemoting = Unity.RenderStreaming.InputSystem.InputRemoting;
using Inputs = UnityEngine.InputSystem.InputSystem;
namespace Unity.RenderStreaming
{
/// <summary>
/// Represents a separate player in the game complete with a set of actions exclusive
/// to the player and a set of paired device.
/// It is the simple version of UnityEngine.InputSystem.PlayerInput that removing dependency of InputControlScheme.
/// </summary>
[AddComponentMenu("Render Streaming/Input Receiver")]
public class InputReceiver : InputChannelReceiverBase
{
internal const string ActionsPropertyName = nameof(m_Actions);
internal const string ActionEventsPropertyName = nameof(m_ActionEvents);
internal const string DefaultActionMapPropertyName = nameof(m_DefaultActionMap);
/// <summary>
/// Event triggered when a device changes.
/// </summary>
public override event Action<InputDevice, InputDeviceChange> onDeviceChange;
/// <summary>
/// Gets or sets the input action asset associated with the player.
/// </summary>
public InputActionAsset actions
{
get
{
if (!m_ActionsInitialized && gameObject.activeSelf)
InitializeActions();
return m_Actions;
}
set
{
if (m_Actions == value)
return;
// Make sure that if we already have actions, they get disabled.
if (m_Actions != null)
{
m_Actions.Disable();
if (m_Enabled)
UninitializeActions();
}
m_Actions = value;
if (m_Enabled)
{
//ClearCaches();
AssignUserAndDevices();
InitializeActions();
if (m_InputActive)
ActivateInput();
}
}
}
/// <summary>
/// Indicates whether the input is currently active.
/// </summary>
public bool inputIsActive => m_InputActive;
/// <summary>
/// Gets or sets the default action map.
/// </summary>
public InputUser user => m_InputUser;
/// <summary>
/// Gets or sets the action events associated with the player.
/// </summary>
public ReadOnlyArray<InputDevice> devices => m_InputUser.pairedDevices;
/// <summary>
/// Gets or sets the current action map.
/// </summary>
public InputActionMap currentActionMap
{
get => m_CurrentActionMap;
set
{
m_CurrentActionMap?.Disable();
m_CurrentActionMap = value;
m_CurrentActionMap?.Enable();
}
}
/// <summary>
/// Gets or sets the default action map.
/// </summary>
public string defaultActionMap
{
get => m_DefaultActionMap;
set => m_DefaultActionMap = value;
}
/// <summary>
/// Gets or sets the action events associated with the player.
/// </summary>
public ReadOnlyArray<PlayerInput.ActionEvent> actionEvents
{
get => m_ActionEvents;
set
{
if (m_Enabled)
UninitializeActions();
m_ActionEvents = value.ToArray();
if (m_Enabled)
InitializeActions();
}
}
protected virtual void OnEnable()
{
m_Enabled = true;
onDeviceChange += OnDeviceChange;
//AssignPlayerIndex();
InitializeActions();
AssignUserAndDevices();
ActivateInput();
}
protected virtual void OnDisable()
{
m_Enabled = false;
onDeviceChange -= OnDeviceChange;
DeactivateInput();
UnassignUserAndDevices();
UninitializeActions();
}
/// <summary>
/// Activates input for the player.
/// </summary>
/// <example>
/// <code>
/// <![CDATA[
/// inputReceiver.ActivateInput();
/// ]]>
///</code>
/// </example>
public void ActivateInput()
{
m_InputActive = true;
// If we have no current action map but there's a default
// action map, make it current.
if (m_CurrentActionMap == null && m_Actions != null && !string.IsNullOrEmpty(m_DefaultActionMap))
SwitchCurrentActionMap(m_DefaultActionMap);
else
m_CurrentActionMap?.Enable();
}
/// <summary>
/// Deactivates input for the player.
/// </summary>
/// <example>
/// <code>
/// <![CDATA[
/// inputReceiver.DeactivateInput();
/// ]]>
///</code>
/// </example>
public void DeactivateInput()
{
m_CurrentActionMap?.Disable();
m_InputActive = false;
}
/// <summary>
/// Switches the current action map to the one with the given name or ID.
/// </summary>
/// <param name="mapNameOrId">The name or ID of the action map to switch to.</param>
/// <example>
/// <code>
/// <![CDATA[
/// inputReceiver.SwitchCurrentActionMap("Gameplay");
///]]>
/// </code>
/// </example>
public void SwitchCurrentActionMap(string mapNameOrId)
{
// Must be enabled.
if (!m_Enabled)
{
RenderStreaming.Logger.Log(LogType.Error, (object)$"Cannot switch to actions '{mapNameOrId}'; input is not enabled", this);
return;
}
// Must have actions.
if (m_Actions == null)
{
RenderStreaming.Logger.Log(LogType.Error, (object)$"Cannot switch to actions '{mapNameOrId}'; no actions set on PlayerInput", this);
return;
}
// Must have map.
var actionMap = m_Actions.FindActionMap(mapNameOrId);
if (actionMap == null)
{
RenderStreaming.Logger.Log(LogType.Error, (object)$"Cannot find action map '{mapNameOrId}' in actions '{m_Actions}'", this);
return;
}
currentActionMap = actionMap;
}
/// <summary>
/// Performs pairing with the specified input device.
/// </summary>
/// <example>
/// <code>
/// <![CDATA[
/// var playerInput = hostPlayer.GetComponent<InputReceiver>();
/// playerInput.PerformPairingWithDevice(device);
/// ]]>
///</code>
/// </example>
/// <param name="device">The input device to pair with.</param>
public void PerformPairingWithDevice(InputDevice device)
{
m_InputUser = InputUser.PerformPairingWithDevice(device, m_InputUser);
}
/// <summary>
/// Performs pairing with all local devices.
/// </summary>
/// <example>
/// <code>
/// <![CDATA[
/// var playerInput = hostPlayer.GetComponent<InputReceiver>();
/// playerInput.PerformPairingWithAllLocalDevices();
/// ]]>
///</code>
/// </example>
public void PerformPairingWithAllLocalDevices()
{
foreach (var device in Inputs.devices.Where(_ => !_.remote))
{
PerformPairingWithDevice(device);
}
}
/// <summary>
/// Unpairs the input user with the given device.
/// </summary>
/// <example>
/// <code>
/// <![CDATA[
/// var playerInput = hostPlayer.GetComponent<InputReceiver>();
/// playerInput.UnpairDevices(device);
/// ]]>
///</code>
/// </example>
/// <param name="device">The device to unpair.</param>
public void UnpairDevices(InputDevice device)
{
if (!m_InputUser.valid)
return;
m_InputUser.UnpairDevice(device);
}
/// <summary>
/// Sets the RTCDataChannel for the sender.
/// </summary>
/// <example>
/// <code>
/// <![CDATA[
/// public void OnAddChannel(SignalingEventData data)
/// {
/// var obj = dictObj[data.connectionId];
/// var channels = obj.GetComponentsInChildren<IDataChannel>();
/// var channel = channels.FirstOrDefault(_ => !_.IsLocal && !_.IsConnected);
/// channel?.SetChannel(data);
/// }
/// ]]>
///</code>
/// </example>
/// <param name="connectionId">The connection ID.</param>
/// <param name="channel">The RTCDataChannel to set.</param>
public override void SetChannel(string connectionId, RTCDataChannel channel)
{
if (channel == null)
{
Dispose();
}
else
{
receiver = new Receiver(channel);
receiver.onDeviceChange += onDeviceChange;
receiverInput = new InputRemoting(receiver);
subscriberDisposer = receiverInput.Subscribe(receiverInput);
receiverInput.StartSending();
}
base.SetChannel(connectionId, channel);
}
/// <summary>
/// Calculates the input region based on the given texture size and region in world coordinates.
/// </summary>
/// <example>
/// <code>
/// <![CDATA[
/// var (region, size) = remoteVideoImage.GetRegionAndSize();
/// inputReceiver.CalculateInputRegion(region, size);
/// ]]>
///</code>
/// </example>
/// <param name="region">The region of the texture in world coordinate system.</param>
/// <param name="size">The size of the texture.</param>
public void CalculateInputRegion(Vector2Int size, Rect region)
{
receiver.CalculateInputRegion(new Rect(Vector2.zero, size), region);
}
/// <summary>
/// Enables or disables input position correction.
/// </summary>
/// <example>
/// <code>
/// <![CDATA[
/// inputReceiver.EnableInputPositionCorrection(true);
/// ]]>
///</code>
/// </example>
/// <param name="enabled">True to enable input position correction, false to disable.</param>
public void SetEnableInputPositionCorrection(bool enabled)
{
receiver.EnableInputPositionCorrection = enabled;
}
protected virtual void OnDestroy()
{
Dispose();
}
protected virtual void Dispose()
{
receiverInput?.StopSending();
subscriberDisposer?.Dispose();
receiver?.Dispose();
receiver = null;
}
[Tooltip("Input actions associated with the player.")]
[SerializeField] internal InputActionAsset m_Actions;
[SerializeField] internal PlayerInput.ActionEvent[] m_ActionEvents;
[SerializeField] internal string m_DefaultActionMap;
[NonSerialized] internal InputActionMap m_CurrentActionMap;
[NonSerialized] private bool m_InputActive;
[NonSerialized] private bool m_Enabled;
[NonSerialized] private bool m_ActionsInitialized;
[NonSerialized] private InputUser m_InputUser;
[NonSerialized] private Receiver receiver;
[NonSerialized] private InputRemoting receiverInput;
[NonSerialized] private IDisposable subscriberDisposer;
private void AssignUserAndDevices()
{
// If we already have a user at this point, clear out all its paired devices
// to start the pairing process from scratch.
if (m_InputUser.valid)
m_InputUser.UnpairDevices();
// All our input goes through actions so there's no point setting
// anything up if we have none.
if (m_Actions == null)
{
// Make sure user is invalid.
m_InputUser = new InputUser();
return;
}
m_InputUser = InputUser.CreateUserWithoutPairedDevices();
// If we don't have a valid user at this point, we don't have any paired devices.
if (m_InputUser.valid)
m_InputUser.AssociateActionsWithUser(actions);
}
private void UnassignUserAndDevices()
{
m_InputUser.UnpairDevicesAndRemoveUser();
}
private void InitializeActions()
{
if (m_ActionsInitialized)
return;
if (m_Actions == null)
return;
var oldActions = m_Actions;
m_Actions = Instantiate(m_Actions);
for (var actionMap = 0; actionMap < oldActions.actionMaps.Count; actionMap++)
{
for (var binding = 0; binding < oldActions.actionMaps[actionMap].bindings.Count; binding++)
m_Actions.actionMaps[actionMap].ApplyBindingOverride(binding, oldActions.actionMaps[actionMap].bindings[binding]);
}
// Hook up all action events.
if (m_ActionEvents != null)
{
foreach (var actionEvent in m_ActionEvents)
{
var id = actionEvent.actionId;
if (string.IsNullOrEmpty(id))
continue;
// Find action for event.
var action = m_Actions.FindAction(id);
if (action != null)
{
////REVIEW: really wish we had a single callback
action.performed += actionEvent.Invoke;
action.canceled += actionEvent.Invoke;
action.started += actionEvent.Invoke;
}
else
{
// Cannot find action. Log error.
if (!string.IsNullOrEmpty(actionEvent.actionName))
{
// We have an action name. Show in message.
RenderStreaming.Logger.Log(LogType.Error,
(object)$"Cannot find action '{actionEvent.actionName}' with ID '{actionEvent.actionId}' in '{m_Actions}",
this);
}
else
{
// We have no action name. Best we have is ID.
RenderStreaming.Logger.Log(LogType.Error,
(object)$"Cannot find action with ID '{actionEvent.actionId}' in '{m_Actions}",
this);
}
}
}
}
m_ActionsInitialized = true;
}
private void UninitializeActions()
{
if (!m_ActionsInitialized)
return;
if (m_Actions == null)
return;
//UninstallOnActionTriggeredHook();
if (m_ActionEvents != null)
{
foreach (var actionEvent in m_ActionEvents)
{
var id = actionEvent.actionId;
if (string.IsNullOrEmpty(id))
continue;
// Find action for event.
var action = m_Actions.FindAction(id);
if (action != null)
{
////REVIEW: really wish we had a single callback
action.performed -= actionEvent.Invoke;
action.canceled -= actionEvent.Invoke;
action.started -= actionEvent.Invoke;
}
}
}
m_CurrentActionMap = null;
m_ActionsInitialized = false;
}
protected virtual void OnDeviceChange(InputDevice device, InputDeviceChange change)
{
switch (change)
{
case InputDeviceChange.Added:
PerformPairingWithDevice(device);
return;
case InputDeviceChange.Removed:
UnpairDevices(device);
return;
}
}
}
}

View File

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

View File

@@ -0,0 +1,110 @@
using System;
using Unity.RenderStreaming.InputSystem;
using Unity.WebRTC;
using UnityEngine;
namespace Unity.RenderStreaming
{
/// <summary>
/// The InputSender component is responsible for sending input data over a data channel in a Unity Render Streaming context.
/// </summary>
/// <seealso cref="InputRemoting" />
[AddComponentMenu("Render Streaming/Input Sender")]
public class InputSender : DataChannelBase
{
private Sender sender;
private InputRemoting senderInput;
private IDisposable suscriberDisposer;
/// <summary>
/// Sets the RTCDataChannel for the sender.
/// </summary>
/// <example>
/// <code>
/// <![CDATA[
/// public void OnAddChannel(SignalingEventData data)
/// {
/// var obj = dictObj[data.connectionId];
/// var channels = obj.GetComponentsInChildren<IDataChannel>();
/// var channel = channels.FirstOrDefault(_ => !_.IsLocal && !_.IsConnected);
/// channel?.SetChannel(data);
/// }
/// ]]>
///</code>
/// </example>
/// <param name="connectionId">The connection ID.</param>
/// <param name="channel">The RTCDataChannel to set.</param>
public override void SetChannel(string connectionId, RTCDataChannel channel)
{
if (channel == null)
{
Dispose();
}
else
{
sender = new Sender();
senderInput = new InputRemoting(sender);
suscriberDisposer = senderInput.Subscribe(new Observer(channel));
channel.OnOpen += OnOpen;
channel.OnClose += OnClose;
}
base.SetChannel(connectionId, channel);
}
/// <summary>
/// Calculates the input region based on the given texture size and region in world coordinates.
/// </summary>
/// <example>
/// <code>
/// <![CDATA[
/// var (region, size) = remoteVideoImage.GetRegionAndSize();
/// inputSender.CalculateInputResion(region, size);
/// ]]>
///</code>
/// </example>
/// <param name="region">The region of the texture in world coordinate system.</param>
/// <param name="size">The size of the texture.</param>
public void CalculateInputResion(Rect region, Vector2Int size)
{
sender.CalculateInputRegion(region, new Rect(Vector2.zero, size));
}
/// <summary>
/// Enables or disables input position correction.
/// </summary>
/// <example>
/// <code>
/// <![CDATA[
/// inputSender.EnableInputPositionCorrection(true);
/// ]]>
///</code>
/// </example>
/// <param name="enabled">True to enable input position correction, false to disable.</param>
public void EnableInputPositionCorrection(bool enabled)
{
sender.EnableInputPositionCorrection = enabled;
}
void OnOpen()
{
senderInput.StartSending();
}
void OnClose()
{
senderInput.StopSending();
}
protected virtual void OnDestroy()
{
this.Dispose();
}
protected void Dispose()
{
senderInput?.StopSending();
suscriberDisposer?.Dispose();
sender?.Dispose();
sender = null;
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,111 @@
// note:: This script is using code snippets in InputSystem.
// https://github.com/Unity-Technologies/InputSystem/blob/develop/Packages/com.unity.inputsystem/InputSystem/Utilities/ArrayHelpers.cs
// todo(kazuki):: This script should be moved into the WebRTC package.
// #if UNITY_WEBRTC_ENABLE_INPUT_SYSTEM
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
// namespace Unity.WebRTC.InputSystem
namespace Unity.RenderStreaming
{
internal static class ArrayHelpers
{
public static int LengthSafe<TValue>(this TValue[] array)
{
if (array == null)
return 0;
return array.Length;
}
public static int Append<TValue>(ref TValue[] array, TValue value)
{
if (array == null)
{
array = new TValue[1];
array[0] = value;
return 0;
}
var length = array.Length;
Array.Resize(ref array, length + 1);
array[length] = value;
return length;
}
public static int Append<TValue>(ref TValue[] array, IEnumerable<TValue> values)
{
if (array == null)
{
array = values.ToArray();
return 0;
}
var oldLength = array.Length;
var valueCount = values.Count();
Array.Resize(ref array, oldLength + valueCount);
var index = oldLength;
foreach (var value in values)
array[index++] = value;
return oldLength;
}
public static int IndexOf<TValue>(TValue[] array, TValue value, int startIndex = 0, int count = -1)
{
if (array == null)
return -1;
if (count < 0)
count = array.Length - startIndex;
var comparer = EqualityComparer<TValue>.Default;
for (var i = startIndex; i < startIndex + count; ++i)
if (comparer.Equals(array[i], value))
return i;
return -1;
}
public static bool Erase<TValue>(ref TValue[] array, TValue value)
{
var index = IndexOf(array, value);
if (index != -1)
{
EraseAt(ref array, index);
return true;
}
return false;
}
public static void EraseAt<TValue>(ref TValue[] array, int index)
{
Debug.Assert(array != null);
Debug.Assert(index >= 0 && index < array.Length);
var length = array.Length;
if (index == 0 && length == 1)
{
array = null;
return;
}
if (index < length - 1)
Array.Copy(array, index + 1, array, index, length - index - 1);
Array.Resize(ref array, length - 1);
}
public static void PutAtIfNotSet<TValue>(ref TValue[] array, int index, Func<TValue> valueFn)
{
if (array.LengthSafe() < index + 1)
Array.Resize(ref array, index + 1);
if (EqualityComparer<TValue>.Default.Equals(array[index], default(TValue)))
array[index] = valueFn();
}
}
}
// #endif

View File

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

View File

@@ -0,0 +1,257 @@
using System.Collections.Generic;
#if URS_USE_TEXTMESHPRO
using TMPro;
#endif
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.LowLevel;
namespace Unity.RenderStreaming.InputSystem
{
/// <summary>
/// This partial class is for workaround to support Unity UI InputField.
/// </summary>
partial class Receiver
{
private static readonly Dictionary<int, KeyCode> s_KeyMap = new Dictionary<int, KeyCode>()
{
{ (int)Key.Backspace, KeyCode.Backspace },
{ (int)Key.Tab, KeyCode.Tab },
{ (int)Key.Enter, KeyCode.Return },
{ (int)Key.Space, KeyCode.Space },
{ (int)Key.Comma, KeyCode.Comma },
{ (int)Key.Minus, KeyCode.Minus },
{ (int)Key.Period, KeyCode.Period },
{ (int)Key.Slash, KeyCode.Slash },
{ (int)Key.Digit0, KeyCode.Alpha0 },
{ (int)Key.Digit1, KeyCode.Alpha1 },
{ (int)Key.Digit2, KeyCode.Alpha2 },
{ (int)Key.Digit3, KeyCode.Alpha3 },
{ (int)Key.Digit4, KeyCode.Alpha4 },
{ (int)Key.Digit5, KeyCode.Alpha5 },
{ (int)Key.Digit6, KeyCode.Alpha6 },
{ (int)Key.Digit7, KeyCode.Alpha7 },
{ (int)Key.Digit8, KeyCode.Alpha8 },
{ (int)Key.Digit9, KeyCode.Alpha9 },
{ (int)Key.Semicolon, KeyCode.Semicolon },
{ (int)Key.Equals, KeyCode.Equals },
{ (int)Key.LeftBracket, KeyCode.LeftBracket },
{ (int)Key.Backslash, KeyCode.Backslash },
{ (int)Key.RightBracket, KeyCode.RightBracket },
{ (int)Key.Backquote, KeyCode.BackQuote },
{ (int)Key.Quote, KeyCode.Quote },
{ (int)Key.A, KeyCode.A },
{ (int)Key.B, KeyCode.B },
{ (int)Key.C, KeyCode.C },
{ (int)Key.D, KeyCode.D },
{ (int)Key.E, KeyCode.E },
{ (int)Key.F, KeyCode.F },
{ (int)Key.G, KeyCode.G },
{ (int)Key.H, KeyCode.H },
{ (int)Key.I, KeyCode.I },
{ (int)Key.J, KeyCode.J },
{ (int)Key.K, KeyCode.K },
{ (int)Key.L, KeyCode.L },
{ (int)Key.M, KeyCode.M },
{ (int)Key.N, KeyCode.N },
{ (int)Key.O, KeyCode.O },
{ (int)Key.P, KeyCode.P },
{ (int)Key.Q, KeyCode.Q },
{ (int)Key.R, KeyCode.R },
{ (int)Key.S, KeyCode.S },
{ (int)Key.T, KeyCode.T },
{ (int)Key.U, KeyCode.U },
{ (int)Key.V, KeyCode.V },
{ (int)Key.W, KeyCode.W },
{ (int)Key.X, KeyCode.X },
{ (int)Key.Y, KeyCode.Y },
{ (int)Key.Z, KeyCode.Z },
{ (int)Key.F1, KeyCode.F1 },
{ (int)Key.F2, KeyCode.F2 },
{ (int)Key.F3, KeyCode.F3 },
{ (int)Key.F4, KeyCode.F4 },
{ (int)Key.F5, KeyCode.F5 },
{ (int)Key.F6, KeyCode.F6 },
{ (int)Key.F7, KeyCode.F7 },
{ (int)Key.F8, KeyCode.F8 },
{ (int)Key.F9, KeyCode.F9 },
{ (int)Key.F10, KeyCode.F10 },
{ (int)Key.F11, KeyCode.F11 },
{ (int)Key.F12, KeyCode.F12 },
{ (int)Key.None, KeyCode.None },
{ (int)Key.LeftArrow, KeyCode.LeftArrow },
{ (int)Key.RightArrow, KeyCode.RightArrow },
{ (int)Key.UpArrow, KeyCode.UpArrow },
{ (int)Key.DownArrow, KeyCode.DownArrow },
{ (int)Key.LeftShift, KeyCode.LeftShift },
{ (int)Key.RightShift, KeyCode.RightShift },
{ (int)Key.Delete, KeyCode.Delete },
{ (int)Key.Escape, KeyCode.Escape },
{ (int)Key.LeftAlt, KeyCode.LeftAlt },
{ (int)Key.RightAlt, KeyCode.RightAlt },
{ (int)Key.LeftApple, KeyCode.LeftApple },
{ (int)Key.RightApple, KeyCode.RightApple }
};
interface IInputField
{
void ProcessEvent(Event e);
void ForceLabelUpdate();
void AppendText(char character);
}
class UGUIInputField : IInputField
{
InputField m_field;
public UGUIInputField(InputField field)
{
m_field = field;
}
public void ProcessEvent(Event e)
{
m_field.ProcessEvent(e);
}
public void ForceLabelUpdate()
{
m_field.ForceLabelUpdate();
}
public void AppendText(char character)
{
m_field.text += character;
}
}
#if URS_USE_TEXTMESHPRO
class TMProInputField : IInputField
{
TMP_InputField m_field;
public TMProInputField(TMP_InputField field)
{
m_field = field;
}
public void ProcessEvent(Event e)
{
m_field.ProcessEvent(e);
}
public void ForceLabelUpdate()
{
m_field.ForceLabelUpdate();
}
public void AppendText(char character)
{
m_field.text += character;
}
}
#endif
IInputField FindInputField(GameObject obj)
{
var field = obj.GetComponent<InputField>();
if (field != null)
return new UGUIInputField(field);
#if URS_USE_TEXTMESHPRO
var tmpField = obj.GetComponent<TMP_InputField>();
if (tmpField != null)
return new TMProInputField(tmpField);
#endif
return null;
}
(EventModifiers, KeyCode) GetEventModifiersAndKeyCode(InputEventPtr ptr)
{
EventModifiers modifiers = EventModifiers.None;
KeyCode keyCode = KeyCode.None;
foreach (var control in ptr.GetAllButtonPresses())
{
if (control is KeyControl keyControl)
{
var key = keyControl.keyCode;
if (key == Key.LeftShift || key == Key.RightShift)
{
modifiers |= EventModifiers.Shift;
}
else if (key == Key.LeftCtrl || key == Key.RightCtrl)
{
modifiers |= EventModifiers.Control;
}
else if (key == Key.LeftAlt || key == Key.RightAlt)
{
modifiers |= EventModifiers.Alt;
}
else if (key == Key.LeftCommand || key == Key.RightCommand)
{
modifiers |= EventModifiers.Command;
}
else if (key == Key.CapsLock)
{
modifiers |= EventModifiers.CapsLock;
}
else if (s_KeyMap.TryGetValue((int)key, out var value))
{
keyCode = value;
}
}
}
return (modifiers, keyCode);
}
unsafe Event CreateEvent(InputEventPtr ptr)
{
var (modifiers, keyCode) = GetEventModifiersAndKeyCode(ptr);
if (ptr.type == TextEvent.Type)
{
var textEventPtr = (TextEvent*)ptr.ToPointer();
var utf32Char = textEventPtr->character;
if (utf32Char >= 0x10000)
{
// todo: not supported multibyte character.
return null;
}
if (utf32Char < 0x100)
{
// ignore control
if (char.IsControl((char)utf32Char))
return null;
}
return new Event
{
type = EventType.KeyDown,
character = (char)utf32Char,
keyCode = keyCode,
modifiers = modifiers
};
}
return new Event
{
type = EventType.KeyDown,
keyCode = keyCode,
modifiers = modifiers
};
}
private void EmulateInputFieldEvent(InputEventPtr ptr)
{
var obj = UnityEngine.EventSystems.EventSystem.current?.currentSelectedGameObject;
if (obj == null)
return;
var field = FindInputField(obj);
if (field == null)
return;
Event e = CreateEvent(ptr);
if (e != null)
{
field.ProcessEvent(e);
field.ForceLabelUpdate();
}
}
}
}

View File

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

View File

@@ -0,0 +1,52 @@
using System;
using System.Reflection;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Layouts;
namespace Unity.RenderStreaming.InputSystem
{
// todo(kazuki)::Avoid to use reflection
static class InputDeviceExtension
{
private static Type typeInputDevice;
private static FieldInfo fieldInfoParticipantId;
private static FieldInfo fieldInfoDescription;
private static FieldInfo fieldInfoDeviceFlags;
static InputDeviceExtension()
{
typeInputDevice = typeof(InputDevice);
fieldInfoParticipantId = typeInputDevice.GetField("m_ParticipantId",
BindingFlags.NonPublic | BindingFlags.Instance);
fieldInfoDescription = typeInputDevice.GetField("m_Description",
BindingFlags.NonPublic | BindingFlags.Instance);
fieldInfoDeviceFlags = typeInputDevice.GetField("m_DeviceFlags",
BindingFlags.NonPublic | BindingFlags.Instance);
}
public static void SetParticipantId(this InputDevice device, int value)
{
fieldInfoParticipantId.SetValue(device, value);
}
public static int GetParticipantId(this InputDevice device)
{
return (int)fieldInfoParticipantId.GetValue(device);
}
public static void SetDescription(this InputDevice device, InputDeviceDescription value)
{
fieldInfoDescription.SetValue(device, value);
}
public static void SetDeviceFlags(this InputDevice device, int value)
{
fieldInfoDeviceFlags.SetValue(device, value);
}
public static int GetDeviceFlags(this InputDevice device)
{
return (int)fieldInfoDeviceFlags.GetValue(device);
}
}
}
// #endif

View File

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

View File

@@ -0,0 +1,45 @@
using System;
using System.IO;
using System.Reflection;
namespace Unity.RenderStreaming.InputSystem
{
#if UNITY_EDITOR && !INPUTSYSTEM_1_1_OR_NEWER
// todo(kazuki)::Avoid to use reflection
static class InputEditorUserSettings
{
private static Type type;
private static PropertyInfo propertyLockInputToGameView;
private static MethodInfo methodLoad;
private static FieldInfo fieldFilePath;
static InputEditorUserSettings()
{
type = Type.GetType("UnityEngine.InputSystem.Editor.InputEditorUserSettings, Unity.InputSystem");
propertyLockInputToGameView = type.GetProperty("lockInputToGameView");
methodLoad = type.GetMethod("Load",
BindingFlags.NonPublic | BindingFlags.Static);
fieldFilePath = type.GetField("kSavePath",
BindingFlags.NonPublic | BindingFlags.Static);
}
public static bool lockInputToGameView
{
get { return (bool)propertyLockInputToGameView.GetValue(null); }
set { propertyLockInputToGameView.SetValue(null, value); }
}
public static void Load()
{
methodLoad.Invoke(null, null);
}
public static void Delete()
{
string filePath = (string)fieldFilePath.GetValue(null);
if(File.Exists(filePath))
File.Delete(filePath);
}
}
#endif
}

View File

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

View File

@@ -0,0 +1,178 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
namespace Unity.RenderStreaming.InputSystem
{
using InputSystem = UnityEngine.InputSystem.InputSystem;
/// <summary>
///
/// </summary>
public interface IInputManager
{
/// <summary>
///
/// </summary>
event Action<InputRemoting.Message> onMessage;
/// <summary>
///
/// </summary>
event Action<InputEventPtr, InputDevice> onEvent;
/// <summary>
///
/// </summary>
event Action<InputDevice, InputDeviceChange> onDeviceChange;
/// <summary>
///
/// </summary>
event Action<string, InputControlLayoutChange> onLayoutChange;
/// <summary>
///
/// </summary>
ReadOnlyArray<InputDevice> devices { get; }
/// <summary>
///
/// </summary>
IEnumerable<string> layouts { get; }
/// <summary>
///
/// </summary>
/// <param name="deviceId"></param>
/// <returns></returns>
InputDevice GetDeviceById(int deviceId);
/// <summary>
///
/// </summary>
/// <param name="layout"></param>
/// <param name="name"></param>
/// <param name="variants"></param>
/// <returns></returns>
InputDevice AddDevice(string layout, string name = null, string variants = null);
/// <summary>
///
/// </summary>
/// <param name="device"></param>
void RemoveDevice(InputDevice device);
/// <summary>
///
/// </summary>
/// <param name="device"></param>
/// <param name="usage"></param>
void AddDeviceUsage(InputDevice device, string usage);
/// <summary>
///
/// </summary>
/// <param name="device"></param>
/// <param name="usage"></param>
void RemoveDeviceUsage(InputDevice device, string usage);
/// <summary>
///
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
InputControlLayout LoadLayout(string name);
/// <summary>
///
/// </summary>
/// <param name="json"></param>
/// <param name="name"></param>
/// <param name="matches"></param>
void RegisterControlLayout(string json, string name = null, bool isOverride = false);
/// <summary>
///
/// </summary>
/// <param name="name"></param>
void RemoveLayout(string name);
/// <summary>
///
/// </summary>
/// <param name="eventPtr"></param>
void QueueEvent(InputEventPtr eventPtr);
}
public abstract class InputManager : IInputManager
{
//todo(kazuki):: remove warning CS0067
#pragma warning disable 0067
public virtual event Action<InputRemoting.Message> onMessage;
public virtual event Action<InputEventPtr, InputDevice> onEvent;
public virtual event Action<InputDevice, InputDeviceChange> onDeviceChange;
public virtual event Action<string, InputControlLayoutChange> onLayoutChange;
#pragma warning restore 0067
public virtual ReadOnlyArray<InputDevice> devices
{
get
{
return InputSystem.devices;
}
}
public virtual IEnumerable<string> layouts
{
get
{
return InputSystem.ListLayouts();
}
}
public virtual InputDevice GetDeviceById(int deviceId)
{
return InputSystem.GetDeviceById(deviceId);
}
public virtual InputDevice AddDevice(string layout, string name = null, string variants = null)
{
foreach (var device_ in InputSystem.devices.Where(device => device.enabled))
InputSystem.ResetDevice(device_);
return InputSystem.AddDevice(layout, name, variants);
}
public virtual void RemoveDevice(InputDevice device)
{
foreach (var device_ in InputSystem.devices.Where(device => device.enabled))
InputSystem.ResetDevice(device_);
InputSystem.RemoveDevice(device);
}
public virtual void AddDeviceUsage(InputDevice device, string usage)
{
InputSystem.AddDeviceUsage(device, usage);
}
public virtual void RemoveDeviceUsage(InputDevice device, string usage)
{
InputSystem.RemoveDeviceUsage(device, usage);
}
public virtual InputControlLayout LoadLayout(string name)
{
return InputSystem.LoadLayout(name);
}
public virtual void RegisterControlLayout(string json, string name = null, bool isOverride = false)
{
if (isOverride)
InputSystem.RegisterLayoutOverride(json, name);
else
InputSystem.RegisterLayout(json, name);
}
public virtual void RemoveLayout(string name)
{
InputSystem.RemoveLayout(name);
}
public virtual void QueueEvent(InputEventPtr eventPtr)
{
InputSystem.QueueEvent(eventPtr);
}
}
}
// #endif

View File

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

View File

@@ -0,0 +1,861 @@
// note:: This script is using code snippets in InputSystem.
// https://github.com/Unity-Technologies/InputSystem/blob/develop/Packages/com.unity.inputsystem/InputSystem/Devices/Remote/InputRemoting.cs
using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Layouts;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
////TODO: show remote device IDs in the debugger
////TODO: remote timestamps need to be translated to local timestamps; doesn't make sense for remote events getting
//// processed on the local timeline as is when the originating timeline may be quite different
////TODO: support actions
////TODO: support input users
////TODO: text input events
////REVIEW: it seems that the various XXXMsg struct should be public; ATM doesn't seem like working with the message interface is practical
////REVIEW: the namespacing mechanism for layouts which changes base layouts means that layouts can't be played
//// around with on the editor side but will only be changed once they're updated in the player
namespace Unity.RenderStreaming.InputSystem
{
/// <summary>
/// Makes the activity and data of an InputManager observable in message form.
/// </summary>
/// <remarks>
/// Can act as both the sender and Receiver of these message so the flow is fully bidirectional,
/// i.e. the InputManager on either end can mirror its layouts, devices, and events over
/// to the other end. This permits streaming input not just from the player to the editor but
/// also feeding input from the editor back into the player.
///
/// Remoting sits entirely on top of the input system as an optional piece of functionality.
/// In development players and the editor, we enable it automatically but in non-development
/// players it has to be explicitly requested by the user.
///
/// To see devices and input from players in the editor, open the Input Debugger through
/// "Windows >> Input Debugger".
/// </remarks>
/// <seealso cref="InputSystem.remoting"/>
/// \todo Reuse memory allocated for messages instead of allocating separately for each message.
/// \todo Inteface to determine what to mirror from the local manager to the remote system.
public sealed class InputRemoting : IObservable<InputRemoting.Message>, IObserver<InputRemoting.Message>
{
/// <summary>
/// Enumeration of possible types of messages exchanged between two InputRemoting instances.
/// </summary>
public enum MessageType
{
Connect,
Disconnect,
NewLayout,
NewDevice,
NewEvents,
RemoveDevice,
RemoveLayout,
ChangeUsages,
StartSending,
StopSending,
}
/// <summary>
/// A message exchanged between two InputRemoting instances.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct Message
{
/// <summary>
/// For messages coming in, numeric ID of the sender of the message. For messages
/// going out, numeric ID of the targeted Receiver of the message.
/// </summary>
public int participantId;
public MessageType type;
public byte[] data;
}
public bool sending
{
get => (m_Flags & Flags.Sending) == Flags.Sending;
private set
{
if (value)
m_Flags |= Flags.Sending;
else
m_Flags &= ~Flags.Sending;
}
}
static InputRemoting()
{
#if UNITY_EDITOR
//
// note: This lines are for avoiding issues when running the editor
// on background. When moved the focus from the editor, input events
// from another process are ignored.
// Please attention behaviours are difference several platforms when
// moving focus from Unity Editor.
//
// Additionally, The behaviour is changed Unity2021.2 later (using
// InputSystem 1.1). Please see "Background behaviour".
// https://docs.unity3d.com/Packages/com.unity.inputsystem@1.1/manual/Settings.html#background-behavior
#if INPUTSYSTEM_1_1_OR_NEWER
// todo(kazuki):
#else
// Make sure we're not affected by the user giving focus away from the
// game view.
//
InputEditorUserSettings.lockInputToGameView = true;
#endif
#endif
}
internal InputRemoting(IInputManager manager, bool startSendingOnConnect = false)
{
if (manager == null)
throw new ArgumentNullException("manager");
m_LocalManager = manager;
if (startSendingOnConnect)
m_Flags |= Flags.StartSendingOnConnect;
//when listening for newly added layouts, must filter out ones we've added from remote
}
/// <summary>
/// Start sending messages for data and activity in the local input system
/// to observers.
/// </summary>
/// <seealso cref="sending"/>
/// <seealso cref="StopSending"/>
public void StartSending()
{
if (sending)
return;
////TODO: send events in bulk rather than one-by-one
m_LocalManager.onMessage += Send;
m_LocalManager.onEvent += SendEvent;
m_LocalManager.onDeviceChange += SendDeviceChange;
m_LocalManager.onLayoutChange += SendLayoutChange;
sending = true;
SendInitialMessages();
}
public void StopSending()
{
if (!sending)
return;
m_LocalManager.onMessage -= Send;
m_LocalManager.onEvent -= SendEvent;
m_LocalManager.onDeviceChange -= SendDeviceChange;
m_LocalManager.onLayoutChange -= SendLayoutChange;
sending = false;
}
void IObserver<Message>.OnNext(Message msg)
{
switch (msg.type)
{
case MessageType.Connect:
ConnectMsg.Process(this);
break;
case MessageType.Disconnect:
DisconnectMsg.Process(this, msg);
break;
case MessageType.NewLayout:
NewLayoutMsg.Process(this, msg);
break;
case MessageType.RemoveLayout:
RemoveLayoutMsg.Process(this, msg);
break;
case MessageType.NewDevice:
NewDeviceMsg.Process(this, msg);
break;
case MessageType.NewEvents:
NewEventsMsg.Process(this, msg);
break;
case MessageType.ChangeUsages:
ChangeUsageMsg.Process(this, msg);
break;
case MessageType.RemoveDevice:
RemoveDeviceMsg.Process(this, msg);
break;
case MessageType.StartSending:
StartSendingMsg.Process(this);
break;
case MessageType.StopSending:
StopSendingMsg.Process(this);
break;
}
}
void IObserver<Message>.OnError(Exception error)
{
}
void IObserver<Message>.OnCompleted()
{
}
public IDisposable Subscribe(IObserver<Message> observer)
{
if (observer == null)
throw new ArgumentNullException("observer");
var subscriber = new Subscriber { owner = this, observer = observer };
ArrayHelpers.Append(ref m_Subscribers, subscriber);
return subscriber;
}
private void SendInitialMessages()
{
SendAllGeneratedLayouts();
SendAllDevices();
}
private void SendAllGeneratedLayouts()
{
// todo(kazuki)::
// layputBuilders property is not published from InputSystem
//
//foreach (var entry in m_LocalManager.m_Layouts.layoutBuilders)
// SendLayout(entry.Key);
foreach (var layout in m_LocalManager.layouts)
SendLayout(layout);
}
private void SendLayout(string layoutName)
{
if (m_Subscribers == null)
return;
var message = NewLayoutMsg.Create(this, layoutName);
if (message != null)
Send(message.Value);
}
private void SendAllDevices()
{
var devices = m_LocalManager.devices;
foreach (var device in devices)
SendDevice(device);
}
private void SendDevice(InputDevice device)
{
if (m_Subscribers == null)
return;
// Don't mirror remote devices to other remotes.
if (device.remote)
return;
var newDeviceMessage = NewDeviceMsg.Create(device);
Send(newDeviceMessage);
// Send current state. We do this here in this case as the device
// may have been added some time ago and thus have already received events.
var stateEventMessage = NewEventsMsg.CreateStateEvent(device);
Send(stateEventMessage);
}
private unsafe void SendEvent(InputEventPtr eventPtr, InputDevice device)
{
if (m_Subscribers == null)
return;
////REVIEW: we probably want to have better control over this and allow producing local events
//// against remote devices which *are* indeed sent across the wire
// Don't send events that came in from remote devices.
if (device != null && device.remote)
return;
var message = NewEventsMsg.Create(eventPtr.data, 1);
Send(message);
}
private void SendDeviceChange(InputDevice device, InputDeviceChange change)
{
if (m_Subscribers == null)
return;
// Don't mirror remoted devices to other remotes.
if (device.remote)
return;
Message msg;
switch (change)
{
case InputDeviceChange.Added:
msg = NewDeviceMsg.Create(device);
break;
case InputDeviceChange.Removed:
msg = RemoveDeviceMsg.Create(device);
break;
case InputDeviceChange.UsageChanged:
msg = ChangeUsageMsg.Create(device);
break;
default:
return;
}
Send(msg);
}
private void SendLayoutChange(string layout, InputControlLayoutChange change)
{
if (m_Subscribers == null)
return;
Message msg;
switch (change)
{
case InputControlLayoutChange.Added:
case InputControlLayoutChange.Replaced:
var message = NewLayoutMsg.Create(this, layout);
if (message == null)
return;
msg = message.Value;
break;
case InputControlLayoutChange.Removed:
msg = RemoveLayoutMsg.Create(layout);
break;
default:
return;
}
Send(msg);
}
private void Send(Message msg)
{
foreach (var subscriber in m_Subscribers)
subscriber.observer.OnNext(msg);
}
////TODO: with C#7 this should be a ref return
private int FindOrCreateSenderRecord(int senderId)
{
// Try to find existing.
if (m_Senders != null)
{
var senderCount = m_Senders.Length;
for (var i = 0; i < senderCount; ++i)
if (m_Senders[i].senderId == senderId)
return i;
}
// Create new.
var sender = new RemoteSender
{
senderId = senderId,
};
return ArrayHelpers.Append(ref m_Senders, sender);
}
private int FindLocalDeviceId(int remoteDeviceId, int senderIndex)
{
var localDevices = m_Senders[senderIndex].devices;
if (localDevices != null)
{
var numLocalDevices = localDevices.Length;
for (var i = 0; i < numLocalDevices; ++i)
{
if (localDevices[i].remoteId == remoteDeviceId)
return localDevices[i].localId;
}
}
return InputDevice.InvalidDeviceId;
}
private InputDevice TryGetDeviceByRemoteId(int remoteDeviceId, int senderIndex)
{
var localId = FindLocalDeviceId(remoteDeviceId, senderIndex);
return m_LocalManager.GetDeviceById(localId);
}
private Flags m_Flags;
private IInputManager m_LocalManager; // Input system we mirror input from and to.
private Subscriber[] m_Subscribers; // Receivers we send input to.
private RemoteSender[] m_Senders; // Senders we receive input from.
[Flags]
private enum Flags
{
Sending = 1 << 0,
StartSendingOnConnect = 1 << 1
}
// Data we keep about a unique sender that we receive input data
// from. We keep track of the layouts and devices we added to
// the local system.
[Serializable]
internal struct RemoteSender
{
public int senderId;
public string[] layouts;
public RemoteInputDevice[] devices;
}
[Serializable]
internal struct RemoteInputDevice
{
public int remoteId; // Device ID used by sender.
public int localId; // Device ID used by us in local system.
public InputDeviceDescription description;
}
internal class Subscriber : IDisposable
{
public InputRemoting owner;
public IObserver<Message> observer;
public void Dispose()
{
ArrayHelpers.Erase(ref owner.m_Subscribers, this);
}
}
private static class ConnectMsg
{
public static void Process(InputRemoting Receiver)
{
if (Receiver.sending)
{
Receiver.SendAllDevices();
}
else if ((Receiver.m_Flags & Flags.StartSendingOnConnect) == Flags.StartSendingOnConnect)
Receiver.StartSending();
}
}
private static class StartSendingMsg
{
public static void Process(InputRemoting Receiver)
{
Receiver.StartSending();
}
}
private static class StopSendingMsg
{
public static void Process(InputRemoting Receiver)
{
Receiver.StopSending();
}
}
public void RemoveRemoteDevices(int participantId)
{
var senderIndex = FindOrCreateSenderRecord(participantId);
// Remove devices added by remote.
var devices = m_Senders[senderIndex].devices;
if (devices != null)
{
foreach (var remoteDevice in devices)
{
var device = m_LocalManager.GetDeviceById(remoteDevice.localId);
if (device != null)
m_LocalManager.RemoveDevice(device);
}
}
////TODO: remove layouts added by remote
ArrayHelpers.EraseAt(ref m_Senders, senderIndex);
}
private static class DisconnectMsg
{
public static void Process(InputRemoting Receiver, Message msg)
{
Receiver.RemoveRemoteDevices(msg.participantId);
Receiver.StopSending();
}
}
// Tell remote input system that there's a new layout.
private static class NewLayoutMsg
{
[Serializable]
public struct Data
{
public string name;
public string layoutJson;
public bool isOverride;
}
public static Message? Create(InputRemoting sender, string layoutName)
{
// Try to load the layout. Ignore the layout if it couldn't
// be loaded.
InputControlLayout layout;
try
{
layout = sender.m_LocalManager.LoadLayout(new InternedString(layoutName));
if (layout == null)
{
RenderStreaming.Logger.Log(string.Format(
"Could not find layout '{0}' meant to be sent through remote connection; this should not happen",
layoutName));
return null;
}
}
catch (Exception exception)
{
RenderStreaming.Logger.Log(string.Format(
"Could not load layout '{0}'; not sending to remote listeners (exception: {1})", layoutName,
exception));
return null;
}
var data = new Data
{
name = layoutName,
layoutJson = layout.ToJson(),
isOverride = layout.isOverride
};
return new Message
{
type = MessageType.NewLayout,
data = SerializeData(data)
};
}
public static void Process(InputRemoting Receiver, Message msg)
{
var data = DeserializeData<Data>(msg.data);
var senderIndex = Receiver.FindOrCreateSenderRecord(msg.participantId);
var internedLayoutName = new InternedString(data.name);
Receiver.m_LocalManager.RegisterControlLayout(data.layoutJson, data.name, data.isOverride);
ArrayHelpers.Append(ref Receiver.m_Senders[senderIndex].layouts, internedLayoutName);
}
}
private static class RemoveLayoutMsg
{
public static Message Create(string layoutName)
{
var bytes = Encoding.UTF8.GetBytes(layoutName);
return new Message
{
type = MessageType.RemoveLayout,
data = bytes
};
}
public static void Process(InputRemoting Receiver, Message msg)
{
////REVIEW: we probably don't want to do this blindly
var layoutName = Encoding.UTF8.GetString(msg.data);
Receiver.m_LocalManager.RemoveLayout(layoutName);
}
}
// Tell remote input system that there's a new device.
private static class NewDeviceMsg
{
[Serializable]
public struct Data
{
public string name;
public string layout;
public int deviceId;
public string[] usages;
public InputDeviceDescription description;
}
public static Message Create(InputDevice device)
{
Debug.Assert(!device.remote, "Device being sent to remotes should be a local device, not a remote one");
var data = new Data
{
name = device.name,
layout = device.layout,
deviceId = device.deviceId,
description = device.description,
usages = device.usages.Select(x => x.ToString()).ToArray()
};
var json = JsonUtility.ToJson(data);
var bytes = Encoding.UTF8.GetBytes(json);
return new Message
{
type = MessageType.NewDevice,
data = bytes
};
}
public static void Process(InputRemoting Receiver, Message msg)
{
var senderIndex = Receiver.FindOrCreateSenderRecord(msg.participantId);
var data = DeserializeData<Data>(msg.data);
// Make sure we haven't already seen the device.
var devices = Receiver.m_Senders[senderIndex].devices;
if (devices != null)
{
foreach (var entry in devices)
if (entry.remoteId == data.deviceId)
{
RenderStreaming.Logger.Log(LogType.Error, string.Format(
"Already received device with id {0} (layout '{1}', description '{3}) from remote {2}",
data.deviceId,
data.layout, msg.participantId, data.description));
return;
}
}
// Create device.
InputDevice device;
try
{
////REVIEW: this gives remote devices names the same way that local devices receive them; should we make remote status visible in the name?
device = Receiver.m_LocalManager.AddDevice(data.layout, data.name);
}
catch (Exception exception)
{
RenderStreaming.Logger.Log(LogType.Error,
$"Could not create remote device '{data.description}' with layout '{data.layout}' locally (exception: {exception})");
return;
}
// todo(kazuki)::Avoid to use reflection
// device.m_ParticipantId = msg.participantId;
device.SetParticipantId(msg.participantId);
// todo(kazuki)::Avoid to use reflection
// device.m_Description = data.description;
// device.m_DeviceFlags |= InputDevice.DeviceFlags.Remote;
device.SetDescription(data.description);
var deviceFlagsRemote = 1 << 3;
device.SetDeviceFlags(device.GetDeviceFlags() | deviceFlagsRemote);
if (data.usages != null)
foreach (var usage in data.usages)
Receiver.m_LocalManager.AddDeviceUsage(device, usage);
// Remember it.
var record = new RemoteInputDevice
{
remoteId = data.deviceId,
localId = device.deviceId,
description = data.description
};
ArrayHelpers.Append(ref Receiver.m_Senders[senderIndex].devices, record);
}
}
// Tell remote system there's new input events.
private static class NewEventsMsg
{
// todo(kazuki):: not found DeviceResetEvent
//public static unsafe Message CreateResetEvent(InputDevice device, bool isHardReset)
//{
// var resetEvent = DeviceResetEvent.Create(device.deviceId, isHardReset);
// return Create((InputEvent*)UnsafeUtility.AddressOf(ref resetEvent), 1);
//}
public static unsafe Message CreateStateEvent(InputDevice device)
{
using (StateEvent.From(device, out var eventPtr))
return Create(eventPtr.data, 1);
}
public static unsafe Message Create(InputEvent* events, int eventCount)
{
// Find total size of event buffer we need.
var totalSize = 0u;
var eventPtr = new InputEventPtr(events);
for (var i = 0; i < eventCount; ++i, eventPtr = eventPtr.Next())
{
totalSize += eventPtr.sizeInBytes;
}
// Copy event data to buffer. Would be nice if we didn't have to do that
// but unfortunately we need a byte[] and can't just pass the 'events' IntPtr
// directly.
var data = new byte[totalSize];
fixed (byte* dataPtr = data)
{
UnsafeUtility.MemCpy(dataPtr, events, totalSize);
}
// Done.
return new Message
{
type = MessageType.NewEvents,
data = data
};
}
public static unsafe void Process(InputRemoting Receiver, Message msg)
{
var manager = Receiver.m_LocalManager;
fixed (byte* dataPtr = msg.data)
{
var dataEndPtr = new IntPtr(dataPtr + msg.data.Length);
var eventCount = 0;
var eventPtr = new InputEventPtr((InputEvent*)dataPtr);
var senderIndex = Receiver.FindOrCreateSenderRecord(msg.participantId);
while ((Int64)eventPtr.data < dataEndPtr.ToInt64())
{
// Patch up device ID to refer to local device and send event.
var remoteDeviceId = eventPtr.deviceId;
var localDeviceId = Receiver.FindLocalDeviceId(remoteDeviceId, senderIndex);
eventPtr.deviceId = localDeviceId;
if (localDeviceId != InputDevice.InvalidDeviceId)
{
////TODO: add API to send events in bulk rather than one by one
manager.QueueEvent(eventPtr);
}
++eventCount;
eventPtr = eventPtr.Next();
}
}
}
}
private static class ChangeUsageMsg
{
[Serializable]
public struct Data
{
public int deviceId;
public string[] usages;
}
public static Message Create(InputDevice device)
{
var data = new Data
{
deviceId = device.deviceId,
usages = device.usages.Select(x => x.ToString()).ToArray()
};
return new Message
{
type = MessageType.ChangeUsages,
data = SerializeData(data)
};
}
public static void Process(InputRemoting Receiver, Message msg)
{
var senderIndex = Receiver.FindOrCreateSenderRecord(msg.participantId);
var data = DeserializeData<Data>(msg.data);
var device = Receiver.TryGetDeviceByRemoteId(data.deviceId, senderIndex);
if (device != null)
{
foreach (var deviceUsage in device.usages)
{
if (!data.usages.Contains(deviceUsage))
Receiver.m_LocalManager.RemoveDeviceUsage(device, new InternedString(deviceUsage));
}
if (data.usages.Length == 1)
Receiver.m_LocalManager.AddDeviceUsage(device, new InternedString(data.usages[0]));
foreach (var dataUsage in data.usages)
{
var internedDataUsage = new InternedString(dataUsage);
if (!device.usages.Contains(internedDataUsage))
Receiver.m_LocalManager.AddDeviceUsage(device, new InternedString(dataUsage));
}
}
}
}
private static class RemoveDeviceMsg
{
public static Message Create(InputDevice device)
{
return new Message
{
type = MessageType.RemoveDevice,
data = BitConverter.GetBytes(device.deviceId)
};
}
public static void Process(InputRemoting Receiver, Message msg)
{
var senderIndex = Receiver.FindOrCreateSenderRecord(msg.participantId);
var remoteDeviceId = BitConverter.ToInt32(msg.data, 0);
var device = Receiver.TryGetDeviceByRemoteId(remoteDeviceId, senderIndex);
if (device != null)
Receiver.m_LocalManager.RemoveDevice(device);
}
}
private static byte[] SerializeData<TData>(TData data)
{
var json = JsonUtility.ToJson(data);
return Encoding.UTF8.GetBytes(json);
}
private static TData DeserializeData<TData>(byte[] data)
{
var json = Encoding.UTF8.GetString(data);
return JsonUtility.FromJson<TData>(json);
}
#if UNITY_EDITOR || DEVELOPMENT_BUILD
// State we want to take across domain reloads. We can only take some of the
// state across. Subscriptions will be lost and have to be manually restored.
[Serializable]
internal struct SerializedState
{
public int senderId;
public RemoteSender[] senders;
// We can't take these across domain reloads but we want to take them across
// InputSystem.Save/Restore.
[NonSerialized] public Subscriber[] subscribers;
}
internal SerializedState SaveState()
{
return new SerializedState
{
senders = m_Senders,
subscribers = m_Subscribers
};
}
internal void RestoreState(SerializedState state, IInputManager manager)
{
m_LocalManager = manager;
m_Senders = state.senders;
}
#endif
}
}
// #endif

View File

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

View File

@@ -0,0 +1,45 @@
using System.IO;
namespace Unity.RenderStreaming.InputSystem
{
/// <summary>
///
/// </summary>
static class MessageSerializer
{
/// <summary>
///
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
public static byte[] Serialize(ref InputRemoting.Message message)
{
var stream = new MemoryStream();
var writer = new BinaryWriter(stream);
writer.Write(message.participantId);
writer.Write((int)message.type);
writer.Write(message.data.Length);
writer.Write(message.data);
return stream.ToArray();
}
/// <summary>
///
/// </summary>
/// <param name="bytes"></param>
/// <param name="message"></param>
public static void Deserialize(byte[] bytes, out InputRemoting.Message message)
{
var reader = new BinaryReader(new MemoryStream(bytes));
message = default;
message.participantId = reader.ReadInt32();
message.type = (InputRemoting.MessageType)reader.ReadInt32();
int length = reader.ReadInt32();
message.data = reader.ReadBytes(length);
}
}
}
// #endif

View File

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

View File

@@ -0,0 +1,211 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Unity.WebRTC;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
namespace Unity.RenderStreaming.InputSystem
{
using InputSystem = UnityEngine.InputSystem.InputSystem;
/// <summary>
///
/// </summary>
partial class Receiver : InputManager, IDisposable
{
public override event Action<InputRemoting.Message> onMessage;
public new event Action<InputDevice, InputDeviceChange> onDeviceChange;
public new event Action<string, InputControlLayoutChange> onLayoutChange;
private RTCDataChannel _channel;
private readonly List<InputDevice> _remoteDevices = new List<InputDevice>();
private readonly Dictionary<string, string> _remoteLayouts = new Dictionary<string, string>();
private readonly List<string> _registeredRemoteLayout = new List<string>();
private InputPositionCorrector _corrector;
private Action<InputEventPtr, InputDevice> _onEvent;
/// <summary>
///
/// </summary>
public bool EnableInputPositionCorrection { set; get; }
/// <summary>
///
/// </summary>
/// <param name="channel"></param>
public Receiver(RTCDataChannel channel)
{
_channel = channel ?? throw new ArgumentNullException("channel is null");
_channel.OnMessage += OnMessage;
_onEvent = (InputEventPtr ptr, InputDevice device) => { base.QueueEvent(ptr); };
_corrector = new InputPositionCorrector(_onEvent);
}
~Receiver()
{
this.Dispose();
}
public void Dispose()
{
RemoveAllRemoteDevices();
RemoveAllRemoteLayouts();
}
private void OnMessage(byte[] bytes)
{
MessageSerializer.Deserialize(bytes, out var message);
onMessage?.Invoke(message);
}
/// <summary>
///
/// </summary>
public override ReadOnlyArray<InputDevice> devices
{
get
{
// note:: InputRemoting class rejects remote devices when sending device information to the remote peer.
// Avoid to get assert "Device being sent to remotes should be a local device, not a remote one"
return new ReadOnlyArray<InputDevice>();
}
}
/// <summary>
///
/// </summary>
public override IEnumerable<string> layouts
{
get
{
return Enumerable.Empty<string>();
}
}
/// <summary>
///
/// </summary>
public ReadOnlyArray<InputDevice> remoteDevices
{
get
{
return new ReadOnlyArray<InputDevice>(_remoteDevices.ToArray());
}
}
public ReadOnlyArray<string> remoteLayouts
{
get
{
return new ReadOnlyArray<string>(_remoteLayouts.Keys.ToArray());
}
}
/// <summary>
///
/// </summary>
public void RemoveAllRemoteDevices()
{
while (_remoteDevices.Count > 0)
{
RemoveDevice(_remoteDevices[0]);
}
}
public void RemoveAllRemoteLayouts()
{
while (_remoteLayouts.Count > 0)
{
RemoveLayout(_remoteLayouts.First().Key);
}
}
public override InputDevice AddDevice(string layout, string name = null, string variants = null)
{
if (InputSystem.ListLayouts().Count(_ => _ == layout) == 0)
{
if (!_remoteLayouts.TryGetValue(layout, out string json))
throw new InvalidOperationException();
base.RegisterControlLayout(json, layout);
_registeredRemoteLayout.Add(layout);
}
var device = base.AddDevice(layout, name, variants);
_remoteDevices.Add(device);
onDeviceChange?.Invoke(device, InputDeviceChange.Added);
return device;
}
public override void RemoveDevice(InputDevice device)
{
base.RemoveDevice(device);
_remoteDevices.Remove(device);
onDeviceChange?.Invoke(device, InputDeviceChange.Removed);
}
public override void RegisterControlLayout(string json, string name = null, bool isOverride = false)
{
// todo(kazuki):: not call base class
// base.RegisterControlLayout(json, name, isOverride);
_remoteLayouts.Add(name, json);
onLayoutChange?.Invoke(name, InputControlLayoutChange.Added);
}
public override void RemoveLayout(string name)
{
if (_registeredRemoteLayout.Contains(name))
{
base.RemoveLayout(name);
_registeredRemoteLayout.Remove(name);
}
_remoteLayouts.Remove(name);
onLayoutChange?.Invoke(name, InputControlLayoutChange.Removed);
}
public override void QueueEvent(InputEventPtr ptr)
{
InputDevice device = InputSystem.GetDeviceById(ptr.deviceId);
// mapping sender coordinate system to receiver one.
if (EnableInputPositionCorrection && device is Pointer && ptr.IsA<StateEvent>())
{
_corrector.Invoke(ptr, device);
}
else
{
base.QueueEvent(ptr);
}
// workaround:
// UnityEngine.UI.InputField and TMP_InputField depends on Event.PopEvent.
// Event.PopEvent is old event API, therefore EventSystem.QueueEvent doesn't queue events.
var eventType = ptr.type;
if (device is Keyboard &&
(eventType == StateEvent.Type ||
eventType == DeltaStateEvent.Type ||
eventType == TextEvent.Type))
{
EmulateInputFieldEvent(ptr);
}
}
/// <summary>
///
/// </summary>
/// <param name="size">Texture Size.</param>
/// <param name="region">Region of the texture in world coordinate system.</param>
public void CalculateInputRegion(Rect inputRegion, Rect outputRegion)
{
_corrector.inputRegion = inputRegion;
_corrector.outputRegion = outputRegion;
}
}
}
// #endif

View File

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

View File

@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using Unity.WebRTC;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Utilities;
namespace Unity.RenderStreaming.InputSystem
{
using InputSystem = UnityEngine.InputSystem.InputSystem;
class Sender : InputManager, IDisposable
{
public override event Action<InputEventPtr, InputDevice> onEvent;
public override event Action<InputDevice, InputDeviceChange> onDeviceChange;
public override event Action<string, InputControlLayoutChange> onLayoutChange;
private InputPositionCorrector _corrector;
private Action<InputEventPtr, InputDevice> _onEvent;
public Sender()
{
InputSystem.onEvent += OnEvent;
InputSystem.onDeviceChange += OnDeviceChange;
InputSystem.onLayoutChange += OnLayoutChange;
_onEvent = (InputEventPtr ptr, InputDevice device) => { onEvent?.Invoke(ptr, device); };
_corrector = new InputPositionCorrector(_onEvent);
}
~Sender()
{
this.Dispose();
}
public void Dispose()
{
InputSystem.onEvent -= OnEvent;
InputSystem.onDeviceChange -= OnDeviceChange;
InputSystem.onLayoutChange -= OnLayoutChange;
}
/// <summary>
///
/// </summary>
public bool EnableInputPositionCorrection { set; get; }
/// <summary>
///
/// </summary>
/// <param name="inputRegion"></param>
/// <param name="outputRegion"></param>
public void CalculateInputRegion(Rect inputRegion, Rect outputRegion)
{
_corrector.inputRegion = inputRegion;
_corrector.outputRegion = outputRegion;
}
private void OnEvent(InputEventPtr ptr, InputDevice device)
{
// mapping sender coordinate system to receiver one.
if (EnableInputPositionCorrection && device is Pointer && ptr.IsA<StateEvent>())
{
_corrector.Invoke(ptr, device);
}
else
{
onEvent?.Invoke(ptr, device);
}
}
private void OnDeviceChange(InputDevice device, InputDeviceChange change)
{
onDeviceChange?.Invoke(device, change);
}
private void OnLayoutChange(string name, InputControlLayoutChange change)
{
onLayoutChange?.Invoke(name, change);
}
}
/// <summary>
///
/// </summary>
class Observer : IObserver<InputRemoting.Message>
{
private RTCDataChannel _channel;
public Observer(RTCDataChannel channel)
{
_channel = channel ?? throw new ArgumentNullException("channel is null");
}
public void OnNext(InputRemoting.Message value)
{
if (_channel.ReadyState != RTCDataChannelState.Open)
return;
byte[] bytes = MessageSerializer.Serialize(ref value);
_channel.Send(bytes);
}
public void OnCompleted()
{
}
public void OnError(Exception error)
{
}
}
}
// #endif

View File

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

View File

@@ -0,0 +1,303 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.WebRTC;
using UnityEngine;
using UnityEngine.Assertions;
namespace Unity.RenderStreaming
{
internal class PeerConnection : IDisposable
{
public delegate void OnConnectEvent();
public delegate void OnDisconnectEvent();
public delegate void OnDataChannelEvent(RTCDataChannel channel);
public delegate void OnTrackEvent(RTCTrackEvent trackEvent);
public delegate void SendOfferEvent(RTCSessionDescription description);
public delegate void SendAnswerEvent(RTCSessionDescription description);
public delegate void SendCandidateEvent(RTCIceCandidate candidate);
public OnConnectEvent OnConnectHandler;
public OnDisconnectEvent OnDisconnectHandler;
public OnDataChannelEvent OnDataChannelHandler;
public OnTrackEvent OnTrackEventHandler;
public SendOfferEvent SendOfferHandler;
public SendAnswerEvent SendAnswerHandler;
public SendCandidateEvent SendCandidateHandler;
public RTCPeerConnection peer => _peer;
/// <summary>
///
/// </summary>
public bool waitingAnswer
{
get => _waitingAnswer;
private set
{
_waitingAnswer = value;
_timeSinceStartWaitingAnswer =
_waitingAnswer ? Time.realtimeSinceStartup : 0;
}
}
private readonly RTCPeerConnection _peer;
private readonly bool _polite;
private readonly Func<IEnumerator, Coroutine> _startCoroutine;
private readonly Action<Coroutine> _stopCoroutine;
private readonly HashSet<WeakReference<Coroutine>> _processingCoroutineList = new HashSet<WeakReference<Coroutine>>();
// resend offer
private readonly float _resendInterval;
private bool _waitingAnswer;
private float _timeSinceStartWaitingAnswer;
// processing set description
private bool _processingSetDescription;
// processing got description
private bool _ignoreOffer;
private bool _srdAnswerPending;
private bool _disposed = false;
public PeerConnection(bool polite, RTCConfiguration config, float resendInterval, Func<IEnumerator, Coroutine> startCoroutine, Action<Coroutine> stopCoroutine)
{
_polite = polite;
_resendInterval = resendInterval;
_startCoroutine = startCoroutine;
_stopCoroutine = stopCoroutine;
_peer = new RTCPeerConnection(ref config);
_peer.OnDataChannel = channel => OnDataChannelHandler?.Invoke(channel);
_peer.OnIceCandidate = candidate => SendCandidateHandler?.Invoke(candidate);
_peer.OnTrack = trackEvent => OnTrackEventHandler?.Invoke(trackEvent);
_peer.OnConnectionStateChange = state =>
{
switch (state)
{
case RTCPeerConnectionState.Connected:
OnConnectHandler?.Invoke();
break;
case RTCPeerConnectionState.Failed:
OnDisconnectHandler?.Invoke();
break;
}
};
_peer.OnNegotiationNeeded = () => StartCoroutine(OnNegotiationNeeded());
}
private void StartCoroutine(IEnumerator enumerator)
{
var co = _startCoroutine(enumerator);
_processingCoroutineList.RemoveWhere(x => !x.TryGetTarget(out _));
_processingCoroutineList.Add(new WeakReference<Coroutine>(co));
}
~PeerConnection()
{
Dispose();
}
public override string ToString()
{
var str = _polite ? "polite" : "impolite";
return
$"[{str}-{nameof(PeerConnection)} {nameof(_peer.ConnectionState)}:{_peer.ConnectionState} {nameof(_peer.IceConnectionState)}:{_peer.IceConnectionState} {nameof(_peer.SignalingState)}:{_peer.SignalingState} {nameof(_peer.GatheringState)}:{_peer.GatheringState}]";
}
public void Dispose()
{
if (_disposed)
return;
foreach (var weakCo in _processingCoroutineList)
{
if (weakCo.TryGetTarget(out var co))
{
_stopCoroutine?.Invoke(co);
}
}
_processingCoroutineList.Clear();
if (_peer != null)
{
_peer.OnTrack = null;
_peer.OnDataChannel = null;
_peer.OnIceCandidate = null;
_peer.OnNegotiationNeeded = null;
_peer.OnConnectionStateChange = null;
_peer.OnIceConnectionChange = null;
_peer.OnIceGatheringStateChange = null;
_peer.Dispose();
}
_disposed = true;
GC.SuppressFinalize(this);
}
private IEnumerator OnNegotiationNeeded()
{
var waitProcessSetDescription = new WaitWhile(() => _processingSetDescription);
yield return waitProcessSetDescription;
SendOffer();
}
public bool IsConnected()
{
return _peer.ConnectionState == RTCPeerConnectionState.Connected;
}
public bool IsStable()
{
return _peer.SignalingState == RTCSignalingState.Stable ||
(_peer.SignalingState == RTCSignalingState.HaveLocalOffer && _srdAnswerPending);
}
public void SendOffer()
{
if (_processingSetDescription)
{
RenderStreaming.Logger.Log(LogType.Warning, $"{this} already processing other set description");
return;
}
if (!IsStable())
{
if (!_waitingAnswer)
{
throw new InvalidOperationException(
$"{this} sendoffer needs in stable state, current state is {_peer.SignalingState}");
}
var timeout = _timeSinceStartWaitingAnswer + _resendInterval;
if (timeout < Time.realtimeSinceStartup)
{
SendOfferHandler?.Invoke(_peer.LocalDescription);
_timeSinceStartWaitingAnswer = Time.realtimeSinceStartup;
}
return;
}
StartCoroutine(SendOfferCoroutine());
}
private IEnumerator SendOfferCoroutine()
{
Assert.AreEqual(_peer.SignalingState, RTCSignalingState.Stable);
Assert.AreEqual(_processingSetDescription, false);
Assert.AreEqual(waitingAnswer, false);
_processingSetDescription = true;
var opLocalDesc = _peer.SetLocalDescription();
yield return opLocalDesc;
if (opLocalDesc.IsError)
{
RenderStreaming.Logger.Log(LogType.Error, $"{this} {opLocalDesc.Error.message}");
_processingSetDescription = false;
yield break;
}
if (_peer.SignalingState != RTCSignalingState.HaveLocalOffer)
{
_processingSetDescription = false;
yield break;
}
Assert.AreEqual(_peer.LocalDescription.type, RTCSdpType.Offer);
Assert.AreEqual(_peer.SignalingState, RTCSignalingState.HaveLocalOffer);
_processingSetDescription = false;
waitingAnswer = true;
SendOfferHandler?.Invoke(_peer.LocalDescription);
}
public void SendAnswer()
{
if (_processingSetDescription)
{
RenderStreaming.Logger.Log(LogType.Warning, $"{this} already processing other set description");
return;
}
StartCoroutine(SendAnswerCoroutine());
}
private IEnumerator SendAnswerCoroutine()
{
Assert.AreEqual(_peer.SignalingState, RTCSignalingState.HaveRemoteOffer);
Assert.AreEqual(_processingSetDescription, false);
_processingSetDescription = true;
var opLocalDesc = _peer.SetLocalDescription();
yield return opLocalDesc;
if (opLocalDesc.IsError)
{
RenderStreaming.Logger.Log(LogType.Error, $"{this} {opLocalDesc.Error.message}");
_processingSetDescription = false;
yield break;
}
Assert.AreEqual(_peer.LocalDescription.type, RTCSdpType.Answer);
Assert.AreEqual(_peer.SignalingState, RTCSignalingState.Stable);
_processingSetDescription = false;
SendAnswerHandler?.Invoke(_peer.LocalDescription);
}
public IEnumerator OnGotDescription(RTCSessionDescription description, Action onComplete)
{
var waitOtherProcess = new WaitWhile(() => _processingSetDescription);
yield return waitOtherProcess;
_ignoreOffer = description.type == RTCSdpType.Offer && !_polite && (_processingSetDescription || !IsStable());
if (_ignoreOffer)
{
RenderStreaming.Logger.Log(LogType.Warning, $"{this} glare - ignoreOffer.");
yield break;
}
waitingAnswer = false;
_srdAnswerPending = description.type == RTCSdpType.Answer;
_processingSetDescription = true;
var remoteDescOp = _peer.SetRemoteDescription(ref description);
yield return remoteDescOp;
if (remoteDescOp.IsError)
{
RenderStreaming.Logger.Log(LogType.Error, $"{this} {remoteDescOp.Error.message}");
_srdAnswerPending = false;
_processingSetDescription = false;
yield break;
}
_srdAnswerPending = false;
_processingSetDescription = false;
onComplete?.Invoke();
}
public bool OnGotIceCandidate(RTCIceCandidate candidate)
{
if (!_peer.AddIceCandidate(candidate))
{
if (!_ignoreOffer)
RenderStreaming.Logger.Log(LogType.Warning, $"{this} this candidate can't accept on state.");
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fbcbecf9d04e490bae7326f66971399d
timeCreated: 1620856244

View File

@@ -0,0 +1,198 @@
using System;
using System.Linq;
using UnityEngine;
using Object = UnityEngine.Object;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Unity.RenderStreaming
{
#if UNITY_EDITOR
[InitializeOnLoad]
#endif
public static class RenderStreaming
{
internal const string EditorBuildSettingsConfigKey = "com.unity.renderstreaming.settings";
internal const string DefaultRenderStreamingSettingsPath =
"Packages/com.unity.renderstreaming/Runtime/RenderStreamingSettings.asset";
private static RenderStreamingSettings s_settings;
private static GameObject s_automaticStreamingObject;
private static ILogger s_logger;
private static bool m_running;
internal static RenderStreamingSettings Settings
{
get => s_settings;
set
{
if (value == null)
throw new ArgumentNullException(nameof(value));
if (s_settings == value)
return;
// In the editor, we keep track of the settings asset through EditorBuildSettings.
#if UNITY_EDITOR
if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(value)))
{
EditorBuildSettings.AddConfigObject(EditorBuildSettingsConfigKey, value, true);
}
#endif
if (m_running && s_settings.signalingSettings != value.signalingSettings)
{
RenderStreaming.Logger.Log(LogType.Warning, "Signaling settings doesn't change on already started signaling instance.");
}
s_settings = value;
ApplySettings();
}
}
public static bool AutomaticStreaming
{
get => s_settings.automaticStreaming;
set
{
if (s_settings.automaticStreaming == value)
{
return;
}
s_settings.automaticStreaming = value;
ApplySettings();
}
}
public static T GetSignalingSettings<T>() where T : SignalingSettings
{
return s_settings.signalingSettings as T;
}
/// <summary>
/// Get & set the logger to use when logging debug messages inside the RenderStreaming package.
/// By default will use Debug.unityLogger.
/// </summary>
/// <exception cref="ArgumentNullException">Throws if setting a null logger.</exception>
public static ILogger Logger
{
get
{
if (s_logger == null)
{
return Debug.unityLogger;
}
return s_logger;
}
set
{
s_logger = value ?? throw new ArgumentNullException(nameof(value));
}
}
static RenderStreaming()
{
#if UNITY_EDITOR
InitializeInEditor();
#else
m_running = true;
#endif
}
#if UNITY_EDITOR
private static void InitializeInEditor()
{
if (EditorBuildSettings.TryGetConfigObject(EditorBuildSettingsConfigKey, out RenderStreamingSettings settingsAsset))
{
s_settings = settingsAsset;
}
else
{
s_settings = AssetDatabase.LoadAssetAtPath<RenderStreamingSettings>(DefaultRenderStreamingSettingsPath);
}
EditorApplication.playModeStateChanged += change =>
{
m_running = change == PlayModeStateChange.EnteredPlayMode;
};
EditorApplication.projectChanged += () =>
{
if (EditorBuildSettings.TryGetConfigObject(EditorBuildSettingsConfigKey, out RenderStreamingSettings _))
{
return;
}
var value = AssetDatabase.LoadAssetAtPath<RenderStreamingSettings>(DefaultRenderStreamingSettingsPath);
if (value != null)
{
Settings = value;
}
};
}
#endif
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void LoadSettings()
{
if (s_settings == null)
{
s_settings = Resources.FindObjectsOfTypeAll<RenderStreamingSettings>().FirstOrDefault() ??
ScriptableObject.CreateInstance<RenderStreamingSettings>();
}
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void RunInitialize()
{
if (AutomaticStreaming)
{
CreateAutomaticStreaming();
}
}
internal static void ApplySettings()
{
#if UNITY_EDITOR
EditorUtility.SetDirty(s_settings);
#endif
if (!m_running)
{
return;
}
if (s_settings.automaticStreaming && s_automaticStreamingObject == null)
{
CreateAutomaticStreaming();
}
if (!s_settings.automaticStreaming)
{
CleanUpAutomaticStreaming();
}
}
private static void CreateAutomaticStreaming()
{
if (s_automaticStreamingObject != null)
{
Object.DestroyImmediate(s_automaticStreamingObject);
}
s_automaticStreamingObject = new GameObject("AutomaticStreaming");
s_automaticStreamingObject.AddComponent<AutomaticStreaming>();
Object.DontDestroyOnLoad(s_automaticStreamingObject);
}
private static void CleanUpAutomaticStreaming()
{
Object.DestroyImmediate(s_automaticStreamingObject);
s_automaticStreamingObject = null;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c3a5616a55e14772b19ab62fe697eeee
timeCreated: 1674112709

View File

@@ -0,0 +1,22 @@
using UnityEngine;
namespace Unity.RenderStreaming
{
/// <summary>
///
/// </summary>
public class RenderStreamingSettings : ScriptableObject
{
internal const string AutomaticStreamingPropertyName = nameof(automaticStreaming);
internal const string SignalingSettingsPropertyName = nameof(signalingSettings);
/// <summary>
///
/// </summary>
[SerializeField, Tooltip("Automatically performs the necessary setup for streaming and starts streaming.")]
public bool automaticStreaming;
[SerializeReference, SignalingSettings]
public SignalingSettings signalingSettings = new WebSocketSignalingSettings();
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bc8ba17751ad4107a50fa1415017b6b1
timeCreated: 1674111941

View File

@@ -0,0 +1,79 @@
using UnityEngine;
#if URS_USE_HDRP_RUNTIME
#if UNITY_2019_1 || UNITY_2019_2 //HDRP 5.x, 6.x
using UnityEngine.Experimental.Rendering.HDPipeline;
#else //HDRP 7.x and above
using UnityEngine.Rendering.HighDefinition;
#endif
#endif
namespace Unity.RenderStreaming
{
[RequireComponent(typeof(Camera))]
#if URS_USE_HDRP_RUNTIME
[RequireComponent(typeof(HDAdditionalCameraData))]
#endif
internal class RenderTextureBlitter : MonoBehaviour
{
[SerializeField] Camera m_rtCamera = null;
Camera m_cam;
#if URS_USE_HDRP_RUNTIME
HDAdditionalCameraData m_hdData;
#endif
private void OnEnable()
{
m_cam = GetComponent<Camera>();
//Render nothing
m_cam.clearFlags = CameraClearFlags.Nothing;
m_cam.cullingMask = 0;
#if URS_USE_HDRP_RUNTIME
m_hdData = GetComponent<HDAdditionalCameraData>();
m_hdData.fullscreenPassthrough = true;
m_hdData.customRender += BlitRenderStreamingRT;
#elif URS_USE_URP_RUNTIME
UnityEngine.Rendering.RenderPipelineManager.endCameraRendering += OnEndCameraRendering;
#else
Camera.onPreRender += BlitTexture;
#endif
}
private void OnDisable()
{
#if URS_USE_HDRP_RUNTIME
m_hdData.customRender -= BlitRenderStreamingRT;
#elif URS_USE_URP_RUNTIME
UnityEngine.Rendering.RenderPipelineManager.endCameraRendering -= OnEndCameraRendering;
#else
Camera.onPreRender -= BlitTexture;
#endif
}
#if URS_USE_HDRP_RUNTIME
public void BlitRenderStreamingRT(UnityEngine.Rendering.ScriptableRenderContext context, HDCamera cam)
{
Graphics.Blit(m_rtCamera.targetTexture, (RenderTexture)null);
}
#elif URS_USE_URP_RUNTIME
void OnEndCameraRendering(UnityEngine.Rendering.ScriptableRenderContext context, Camera cam)
{
if (cam == m_cam && null != m_rtCamera.targetTexture)
{
//This seems to work only if we have setup PostProcessing Stack V2
Graphics.Blit(m_rtCamera.targetTexture, (RenderTexture)null);
}
}
#else
private void BlitTexture(Camera cam)
{
if (m_cam == cam && null != m_rtCamera.targetTexture)
{
Graphics.Blit(m_rtCamera.targetTexture, (RenderTexture)null);
}
}
#endif
}
}

View File

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fa3d0c210b91472b80c4158cb9459cab
timeCreated: 1586218012

View File

@@ -0,0 +1,413 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using Unity.WebRTC;
using UnityEngine;
namespace Unity.RenderStreaming.Signaling
{
public class HttpSignaling : ISignaling
{
private static HashSet<HttpSignaling> instances = new HashSet<HttpSignaling>();
private readonly string m_url;
private readonly int m_timeout;
private readonly SynchronizationContext m_mainThreadContext;
private bool m_running;
private Thread m_signalingThread;
private string m_sessionId;
private long m_lastTimeGetAllRequest;
public string participantId;
public string Url { get { return m_url; } }
public HttpSignaling(SignalingSettings signalingSettings, SynchronizationContext mainThreadContext)
{
if (signalingSettings == null)
throw new ArgumentNullException(nameof(signalingSettings));
if (!(signalingSettings is HttpSignalingSettings settings))
throw new ArgumentException("signalingSettings is not HttpSignalingSettings");
m_url = settings.url;
m_timeout = settings.interval;
m_mainThreadContext = mainThreadContext;
if (m_url.StartsWith("https"))
{
ServicePointManager.ServerCertificateValidationCallback =
(sender, certificate, chain, errors) => true;
}
if (instances.Any(x => x.Url == m_url))
{
RenderStreaming.Logger.Log(LogType.Warning, $"Other {nameof(HttpSignaling)} exists with same URL:{m_url}. Signaling process may be in conflict.");
}
instances.Add(this);
}
~HttpSignaling()
{
Stop();
instances.Remove(this);
}
public void Start()
{
if (m_running)
throw new InvalidOperationException("This object is already started.");
m_running = true;
m_signalingThread = new Thread(HTTPPolling);
m_signalingThread.IsBackground = true;
m_signalingThread.Start();
}
public void Stop()
{
m_running = false;
if (m_signalingThread != null)
{
try
{
// Note: Allow for twice the configured m_timeout duration when joining to account for the polling sleep
// and the time it takes to send a disconnect to the signaling server.
if (!m_signalingThread.Join(m_timeout * 2))
{
m_signalingThread.Abort();
}
}
catch (Exception e)
{
RenderStreaming.Logger.Log(LogType.Error, "Signaling: HTTP stopping thread error : " + e);
}
m_signalingThread = null;
}
}
public event OnStartHandler OnStart;
public event OnConnectHandler OnCreateConnection;
public event OnDisconnectHandler OnDestroyConnection;
public event OnOfferHandler OnOffer;
public event OnAnswerHandler OnAnswer;
public event OnIceCandidateHandler OnIceCandidate;
public void SendOffer(string connectionId, RTCSessionDescription offer)
{
DescData data = new DescData();
data.connectionId = connectionId;
data.sdp = offer.sdp;
ThreadPool.QueueUserWorkItem(_ => { HTTPPost("signaling/offer", data); });
}
public void SendAnswer(string connectionId, RTCSessionDescription answer)
{
DescData data = new DescData();
data.connectionId = connectionId;
data.sdp = answer.sdp;
ThreadPool.QueueUserWorkItem(_ => { HTTPPost("signaling/answer", data); });
}
public void SendCandidate(string connectionId, RTCIceCandidate candidate)
{
CandidateData data = new CandidateData();
data.connectionId = connectionId;
data.candidate = candidate.Candidate;
data.sdpMLineIndex = candidate.SdpMLineIndex.GetValueOrDefault(0);
data.sdpMid = candidate.SdpMid;
ThreadPool.QueueUserWorkItem(_ => { HTTPPost("signaling/candidate", data); });
}
public void OpenConnection(string connectionId)
{
ThreadPool.QueueUserWorkItem(_ => { HTTPConnect(connectionId); });
}
public void CloseConnection(string connectionId)
{
ThreadPool.QueueUserWorkItem(_ => { HTTPDisonnect(connectionId); });
}
private void HTTPPolling()
{
// ignore messages arrived before 30 secs ago
m_lastTimeGetAllRequest = DateTime.UtcNow.Millisecond - 30000;
while (m_running && string.IsNullOrEmpty(m_sessionId))
{
HTTPCreate();
try
{
Thread.Sleep(m_timeout);
}
catch (ThreadAbortException)
{
// Thread.Abort() called from main thread. Ignore
return;
}
}
while (m_running)
{
try
{
HTTPGetAll();
Thread.Sleep(m_timeout);
}
catch (ThreadAbortException)
{
// Thread.Abort() called from main thread. Ignore
return;
}
catch (Exception e)
{
RenderStreaming.Logger.Log(LogType.Error, "Signaling: HTTP polling error : " + e);
}
}
HTTPDelete();
RenderStreaming.Logger.Log("Signaling: HTTP polling thread ended");
}
private static HttpWebResponse HTTPGetResponse(HttpWebRequest request)
{
try
{
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
if (response.StatusCode == HttpStatusCode.OK)
{
return response;
}
else
{
RenderStreaming.Logger.Log(LogType.Error, $"Signaling: {response.ResponseUri} HTTP request failed ({response.StatusCode})");
response.Close();
}
}
catch (ThreadAbortException)
{
// Thread.Abort() called from main thread. Ignore
}
catch (Exception e)
{
RenderStreaming.Logger.Log(LogType.Error, $"Signaling: HTTP request error. url:{request.RequestUri} exception:{e}");
}
return null;
}
private static T HTTPParseJsonResponse<T>(HttpWebResponse response) where T : class
{
if (response == null) return null;
T obj = null;
using (Stream dataStream = response.GetResponseStream())
{
StreamReader reader = new StreamReader(dataStream);
string responseFromServer = reader.ReadToEnd();
obj = JsonUtility.FromJson<T>(responseFromServer);
}
response.Close();
return obj;
}
private static string HTTPParseTextResponse(HttpWebResponse response)
{
if (response == null) return null;
string str = null;
using (Stream dataStream = response.GetResponseStream())
{
StreamReader reader = new StreamReader(dataStream);
str = reader.ReadToEnd();
}
response.Close();
return str;
}
private bool HTTPCreate()
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create($"{m_url}/signaling");
request.Method = "PUT";
request.ContentType = "application/json";
request.KeepAlive = false;
request.ContentLength = 0;
RenderStreaming.Logger.Log($"Signaling: Connecting HTTP {m_url}");
OpenSessionData resp = HTTPParseJsonResponse<OpenSessionData>(HTTPGetResponse(request));
if (resp != null)
{
m_sessionId = resp.sessionId;
RenderStreaming.Logger.Log("Signaling: HTTP connected, sessionId : " + m_sessionId);
m_mainThreadContext.Post(d => OnStart?.Invoke(this), null);
return true;
}
else
{
return false;
}
}
private bool HTTPDelete()
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create($"{m_url}/signaling");
request.Method = "DELETE";
request.ContentType = "application/json";
request.KeepAlive = false;
request.Headers.Add("Session-Id", m_sessionId);
RenderStreaming.Logger.Log($"Signaling: Removing HTTP connection from {m_url}");
return (HTTPParseTextResponse(HTTPGetResponse(request)) != null);
}
private bool HTTPPost(string path, object data)
{
string str = JsonUtility.ToJson(data);
byte[] bytes = new System.Text.UTF8Encoding().GetBytes(str);
RenderStreaming.Logger.Log("Signaling: Posting HTTP data: " + str);
HttpWebRequest request = (HttpWebRequest)WebRequest.Create($"{m_url}/{path}");
request.Method = "POST";
request.ContentType = "application/json";
request.Headers.Add("Session-Id", m_sessionId);
request.KeepAlive = false;
using (Stream dataStream = request.GetRequestStream())
{
dataStream.Write(bytes, 0, bytes.Length);
dataStream.Close();
}
return (HTTPParseTextResponse(HTTPGetResponse(request)) != null);
}
private bool HTTPConnect(string connectionId)
{
HttpWebRequest request =
(HttpWebRequest)WebRequest.Create($"{m_url}/signaling/connection");
request.Method = "PUT";
request.ContentType = "application/json";
request.Headers.Add("Session-Id", m_sessionId);
request.KeepAlive = false;
using (Stream dataStream = request.GetRequestStream())
{
byte[] bytes = new System.Text.UTF8Encoding().GetBytes($"{{\"connectionId\":\"{connectionId}\"}}");
dataStream.Write(bytes, 0, bytes.Length);
dataStream.Close();
}
HttpWebResponse response = HTTPGetResponse(request);
CreateConnectionResData data = HTTPParseJsonResponse<CreateConnectionResData>(response);
if (data == null) return false;
RenderStreaming.Logger.Log($"Signaling: HTTP create connection, connectionId: {connectionId}, polite:{data.polite}");
m_mainThreadContext.Post(d => OnCreateConnection?.Invoke(this, data.connectionId, data.polite), null);
return true;
}
private bool HTTPDisonnect(string connectionId)
{
HttpWebRequest request =
(HttpWebRequest)WebRequest.Create($"{m_url}/signaling/connection");
request.Method = "Delete";
request.ContentType = "application/json";
request.Headers.Add("Session-Id", m_sessionId);
request.KeepAlive = false;
using (Stream dataStream = request.GetRequestStream())
{
byte[] bytes = new System.Text.UTF8Encoding().GetBytes($"{{\"connectionId\":\"{connectionId}\"}}");
dataStream.Write(bytes, 0, bytes.Length);
dataStream.Close();
}
var data = HTTPParseTextResponse(HTTPGetResponse(request));
if (data == null) return false;
RenderStreaming.Logger.Log("Signaling: HTTP delete connection, connectionId : " + connectionId);
m_mainThreadContext.Post(d => OnDestroyConnection?.Invoke(this, connectionId), null);
return true;
}
private bool HTTPGetAll()
{
HttpWebRequest request =
(HttpWebRequest)WebRequest.Create($"{m_url}/signaling?fromtime={m_lastTimeGetAllRequest}");
request.Method = "GET";
request.ContentType = "application/json";
request.Headers.Add("Session-Id", m_sessionId);
request.KeepAlive = false;
HttpWebResponse response = HTTPGetResponse(request);
AllResData data = HTTPParseJsonResponse<AllResData>(response);
if (data == null) return false;
m_lastTimeGetAllRequest =
long.TryParse(data.datetime, out var result) ? result : DateTime.Now.ToJsMilliseconds();
foreach (var msg in data.messages)
{
if (string.IsNullOrEmpty(msg.type))
continue;
if (msg.type == "disconnect")
{
m_mainThreadContext.Post(d => OnDestroyConnection?.Invoke(this, msg.connectionId), null);
}
else if (msg.type == "offer")
{
DescData offer = new DescData();
offer.connectionId = msg.connectionId;
offer.sdp = msg.sdp;
offer.polite = msg.polite;
m_mainThreadContext.Post(d => OnOffer?.Invoke(this, offer), null);
}
else if (msg.type == "answer")
{
DescData answer = new DescData
{
connectionId = msg.connectionId,
sdp = msg.sdp
};
m_mainThreadContext.Post(d => OnAnswer?.Invoke(this, answer), null);
}
else if (msg.type == "candidate")
{
CandidateData candidate = new CandidateData
{
connectionId = msg.connectionId,
candidate = msg.candidate,
sdpMLineIndex = msg.sdpMLineIndex,
sdpMid = msg.sdpMid
};
m_mainThreadContext.Post(d => OnIceCandidate?.Invoke(this, candidate), null);
}
}
return true;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c4673647ba304609a37e2762bb1fae36
timeCreated: 1586218423

View File

@@ -0,0 +1,130 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Unity.RenderStreaming.Signaling;
using UnityEngine;
namespace Unity.RenderStreaming
{
[Serializable, SignalingType("http")]
public class HttpSignalingSettings : SignalingSettings
{
/// <summary>
///
/// </summary>
public override Type signalingClass => typeof(HttpSignaling);
/// <summary>
///
/// </summary>
public override IReadOnlyCollection<IceServer> iceServers => m_iceServers;
/// <summary>
///
/// </summary>
public string url => m_url;
/// <summary>
/// Polling interval
/// </summary>
public int interval => m_interval;
[SerializeField, Tooltip("Set the polling frequency (in milliseconds) to the signaling server.")]
private int m_interval;
[SerializeField, Tooltip("Set the signaling server URL. you should specify a URL starting with \"http\" or \"https\".")]
protected string m_url;
[SerializeField, Tooltip("Set a list of STUN/TURN servers.")]
protected IceServer[] m_iceServers;
/// <summary>
///
/// </summary>
/// <param name="url"></param>
/// <param name="iceServers"></param>
/// <param name="interval"></param>
public HttpSignalingSettings(string url, IceServer[] iceServers = null, int interval = 5000)
{
if (url == null)
throw new ArgumentNullException("url");
if (!Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute))
throw new ArgumentException("url is not well formed Uri");
m_url = url;
m_iceServers = iceServers == null ? Array.Empty<IceServer>() : iceServers.Select(server => server.Clone()).ToArray();
m_interval = interval;
}
/// <summary>
///
/// </summary>
public HttpSignalingSettings()
{
m_url = "http://127.0.0.1";
m_iceServers = new[]
{
new IceServer (urls: new[] {"stun:stun.l.google.com:19302"})
};
m_interval = 5000;
}
/// <summary>
///
/// </summary>
/// <param name="argumetns"></param>
/// <returns></returns>
public override bool ParseArguments(string[] arguments)
{
if (arguments == null)
throw new ArgumentNullException("arguments");
if (arguments.Length == 0)
throw new ArgumentException("arguments is empty");
if (!CommandLineParser.TryParse(arguments))
return false;
if (CommandLineParser.ImportJson.Value != null)
{
CommandLineInfo info = CommandLineParser.ImportJson.Value.Value;
if (info.signalingUrl != null)
m_url = info.signalingUrl;
if (info.iceServers != null && info.iceServers.Length != 0)
m_iceServers = info.iceServers.Select(v => new IceServer(v)).ToArray();
}
if (CommandLineParser.SignalingUrl.Value != null)
m_url = CommandLineParser.SignalingUrl.Value;
var username = CommandLineParser.IceServerUsername != null
? CommandLineParser.IceServerUsername.Value
: null;
var credential = CommandLineParser.IceServerCredential != null
? CommandLineParser.IceServerCredential.Value
: null;
var credentialType = CommandLineParser.IceServerCredentialType != null
? CommandLineParser.IceServerCredentialType.Value
: null;
var urls = CommandLineParser.IceServerUrls != null
? CommandLineParser.IceServerUrls.Value
: null;
if (m_iceServers.Length > 0)
m_iceServers[0] = m_iceServers[0].Clone(
username: username,
credential: credential,
credentialType: credentialType,
urls: urls);
else
m_iceServers = new IceServer[]
{
new IceServer(
username: username,
credential: credential,
credentialType: credentialType.GetValueOrDefault(),
urls: urls)
};
if (CommandLineParser.PollingInterval.Value != null)
m_interval = CommandLineParser.PollingInterval.Value.Value;
return true;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e6fb58902f9c27e4688c3a6fe70d9560
timeCreated: 1674112040

View File

@@ -0,0 +1,32 @@
using Unity.WebRTC;
namespace Unity.RenderStreaming.Signaling
{
public delegate void OnStartHandler(ISignaling signaling);
public delegate void OnConnectHandler(ISignaling signaling, string connectionId, bool polite);
public delegate void OnDisconnectHandler(ISignaling signaling, string connectionId);
public delegate void OnOfferHandler(ISignaling signaling, DescData e);
public delegate void OnAnswerHandler(ISignaling signaling, DescData e);
public delegate void OnIceCandidateHandler(ISignaling signaling, CandidateData e);
public interface ISignaling
{
void Start();
void Stop();
event OnStartHandler OnStart;
event OnConnectHandler OnCreateConnection;
event OnDisconnectHandler OnDestroyConnection;
event OnOfferHandler OnOffer;
event OnAnswerHandler OnAnswer;
event OnIceCandidateHandler OnIceCandidate;
string Url { get; }
void OpenConnection(string connectionId);
void CloseConnection(string connectionId);
void SendOffer(string connectionId, RTCSessionDescription offer);
void SendAnswer(string connectionId, RTCSessionDescription answer);
void SendCandidate(string connectionId, RTCIceCandidate candidate);
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ac843bc417af42759d8bd83db0c33d76
timeCreated: 1586218157

View File

@@ -0,0 +1,110 @@
using System;
namespace Unity.RenderStreaming
{
#pragma warning disable 0649
[Serializable]
public class DescData
{
public string connectionId;
public string sdp;
public bool polite;
public DateTime dateTime;
}
[Serializable]
public class CandidateData
{
public string connectionId;
public string participantId;
public string candidate;
public string sdpMid;
public int sdpMLineIndex;
}
[Serializable]
class SignalingMessage
{
public string status;
public string message;
public string sessionId;
public string connectionId;
public string participantId;
public bool polite;
public string sdp;
public string type;
public string candidate;
public string sdpMid;
public int sdpMLineIndex;
}
[Serializable]
class RoutedMessage<T>
{
public string from;
public string to;
public string type;
public string participantId;
public T data;
}
[Serializable]
class OpenSessionData
{
public string sessionId;
}
[Serializable]
class CreateConnectionResData
{
public string connectionId;
public bool polite;
}
[Serializable]
class DestroyConnectionResData
{
public string connectionId;
}
[Serializable]
class ConnectionResDataList
{
public DescData[] connections;
}
[Serializable]
class OfferResDataList
{
public DescData[] offers;
}
[Serializable]
class AnswerResDataList
{
public DescData[] answers;
}
[Serializable]
class CandidateContainerResDataList
{
public CandidateContainerResData[] candidates;
}
[Serializable]
class CandidateContainerResData
{
public string connectionId;
public CandidateData[] candidates;
}
[Serializable]
class AllResData
{
public SignalingMessage[] messages;
public string datetime;
}
#pragma warning restore 0649
}

View File

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

View File

@@ -0,0 +1,187 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Unity.WebRTC;
using UnityEngine;
namespace Unity.RenderStreaming
{
/// <summary>
/// The attribute is used for commandline argument of "-signalingType".
/// </summary>
public sealed class SignalingTypeAttribute : Attribute
{
/// <summary>
///
/// </summary>
public string typename => m_typename;
private string m_typename;
/// <summary>
///
/// </summary>
/// <param name="typename"></param>
public SignalingTypeAttribute(string name)
{
m_typename = name;
}
}
internal sealed class SignalingSettingsAttribute : PropertyAttribute { }
/// <summary>
///
/// </summary>
public enum IceCredentialType
{
/// <summary>
///
/// </summary>
Password = 0,
/// <summary>
///
/// </summary>
OAuth = 1
}
/// <summary>
///
/// </summary>
[Serializable]
public class IceServer
{
/// <summary>
///
/// </summary>
public IReadOnlyCollection<string> urls => m_urls;
/// <summary>
///
/// </summary>
public string username => m_username;
/// <summary>
///
/// </summary>
public IceCredentialType credentialType => m_credentialType;
/// <summary>
///
/// </summary>
public string credential => m_credential;
[SerializeField]
private string[] m_urls;
[SerializeField]
private string m_username;
[SerializeField]
private IceCredentialType m_credentialType;
[SerializeField]
private string m_credential;
/// <summary>
///
/// </summary>
/// <param name="server"></param>
public static explicit operator RTCIceServer(IceServer server)
{
var iceServer = new RTCIceServer
{
urls = server.urls.ToArray(),
username = server.username,
credential = server.credential,
credentialType = (RTCIceCredentialType)server.credentialType
};
return iceServer;
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public IceServer Clone()
{
return new IceServer(this.urls.ToArray(), this.username, this.credentialType, this.credential);
}
public IceServer Clone(string[] urls = null, string username = null, IceCredentialType? credentialType = null, string credential = null)
{
var server = new IceServer(this.urls.ToArray(), this.username, this.credentialType, this.credential);
if (urls != null)
server.m_urls = urls;
if (username != null)
server.m_username = username;
if (credentialType != null)
server.m_credentialType = credentialType.Value;
if (credential != null)
server.m_credential = credential;
return server;
}
/// <summary>
///
/// </summary>
/// <param name="urls"></param>
/// <param name="username"></param>
/// <param name="credentialType"></param>
/// <param name="credential"></param>
public IceServer(string[] urls = null, string username = null, IceCredentialType credentialType = IceCredentialType.Password, string credential = null)
{
m_urls = urls?.ToArray();
m_username = username;
m_credential = credential;
m_credentialType = credentialType;
}
internal IceServer(RTCIceServer server)
{
m_urls = server.urls.ToArray();
m_username = server.username;
m_credential = server.credential;
m_credentialType = (IceCredentialType)server.credentialType;
}
}
/// <summary>
///
/// </summary>
public abstract class SignalingSettings
{
/// <summary>
///
/// </summary>
public abstract IReadOnlyCollection<IceServer> iceServers { get; }
/// <summary>
///
/// </summary>
public abstract Type signalingClass { get; }
/// <summary>
///
/// </summary>
/// <param name="arguments"></param>
/// <returns></returns>
public abstract bool ParseArguments(string[] arguments);
}
internal static class RuntimeTypeCache<T> where T : class
{
private static Type[] s_types;
public static Type[] GetTypesDerivedFrom()
{
if (s_types != null)
return s_types;
s_types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(domainAssembly => domainAssembly.GetTypes())
.Where(type => typeof(T).IsAssignableFrom(type) && !type.IsAbstract).ToArray();
return s_types;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ff254cc8ca9e409bbfe92d5c717e71ab
timeCreated: 1674112040

View File

@@ -0,0 +1,17 @@
using UnityEngine;
namespace Unity.RenderStreaming
{
internal class SignalingSettingsObject : ScriptableObject
{
[SerializeReference]
public SignalingSettings settings;
public static SignalingSettingsObject Create()
{
var instance = CreateInstance<SignalingSettingsObject>();
instance.settings = new WebSocketSignalingSettings();
return instance;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 22c294fb286b37446a86664f4fb41023
timeCreated: 1674112040

View File

@@ -0,0 +1,304 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Authentication;
using System.Text;
using System.Threading;
using Unity.WebRTC;
using UnityEngine;
using WebSocketSharp;
namespace Unity.RenderStreaming.Signaling
{
public class WebSocketSignaling : ISignaling
{
private static HashSet<WebSocketSignaling> instances = new HashSet<WebSocketSignaling>();
private readonly string m_url;
private readonly float m_timeout;
private readonly SynchronizationContext m_mainThreadContext;
private bool m_running;
private Thread m_signalingThread;
private readonly AutoResetEvent m_wsCloseEvent;
private WebSocket m_webSocket;
public string participantId;
public string Url { get { return m_url; } }
public WebSocketSignaling(SignalingSettings signalingSettings, SynchronizationContext mainThreadContext)
{
if (signalingSettings == null)
throw new ArgumentNullException(nameof(signalingSettings));
if (!(signalingSettings is WebSocketSignalingSettings settings))
throw new ArgumentException("signalingSettings is not WebSocketSignalingSettings");
m_url = settings.url;
m_timeout = 5.0f;
m_mainThreadContext = mainThreadContext;
m_wsCloseEvent = new AutoResetEvent(false);
if (instances.Any(x => x.Url == m_url))
{
RenderStreaming.Logger.Log(LogType.Warning, $"Other {nameof(WebSocketSignaling)} exists with same URL:{m_url}. Signaling process may be in conflict.");
}
instances.Add(this);
}
~WebSocketSignaling()
{
if (m_running)
Stop();
instances.Remove(this);
}
public void Start()
{
if (m_running)
throw new InvalidOperationException("This object is already started.");
m_running = true;
m_signalingThread = new Thread(WSManage);
m_signalingThread.Start();
}
public void Stop()
{
if (m_running)
{
m_running = false;
m_webSocket?.Close();
if (m_signalingThread.ThreadState == ThreadState.WaitSleepJoin)
{
m_signalingThread.Abort();
}
else
{
m_signalingThread.Join(1000);
}
m_signalingThread = null;
}
}
public event OnStartHandler OnStart;
public event OnConnectHandler OnCreateConnection;
public event OnDisconnectHandler OnDestroyConnection;
public event OnOfferHandler OnOffer;
#pragma warning disable 0067
// this event is never used in this class
public event OnAnswerHandler OnAnswer;
#pragma warning restore 0067
public event OnIceCandidateHandler OnIceCandidate;
public void SendOffer(string connectionId, RTCSessionDescription offer)
{
DescData data = new DescData();
data.connectionId = connectionId;
data.sdp = offer.sdp;
data.dateTime = DateTime.Now;
RoutedMessage<DescData> routedMessage = new RoutedMessage<DescData>();
routedMessage.from = connectionId;
routedMessage.data = data;
routedMessage.type = "offer";
routedMessage.participantId =participantId;
WSSend(routedMessage);
}
public void SendAnswer(string connectionId, RTCSessionDescription answer)
{
DescData data = new DescData();
data.connectionId = connectionId;
data.sdp = answer.sdp;
RoutedMessage<DescData> routedMessage = new RoutedMessage<DescData>();
routedMessage.from = connectionId;
routedMessage.data = data;
routedMessage.type = "answer";
routedMessage.participantId =participantId;
WSSend(routedMessage);
}
public void SendCandidate(string connectionId, RTCIceCandidate candidate)
{
CandidateData data = new CandidateData();
data.connectionId = connectionId;
data.candidate = candidate.Candidate;
data.sdpMLineIndex = candidate.SdpMLineIndex.GetValueOrDefault(0);
data.sdpMid = candidate.SdpMid;
RoutedMessage<CandidateData> routedMessage = new RoutedMessage<CandidateData>();
routedMessage.from = connectionId;
routedMessage.data = data;
routedMessage.type = "candidate";
WSSend(routedMessage);
}
public void OpenConnection(string connectionId)
{
this.WSSend($"{{\"type\":\"connect\", \"connectionId\":\"{connectionId}\"}}");
}
public void CloseConnection(string connectionId)
{
this.WSSend($"{{\"type\":\"disconnect\", \"connectionId\":\"{connectionId}\"}}");
}
private void WSManage()
{
while (m_running)
{
WSCreate();
try
{
m_wsCloseEvent.WaitOne();
Thread.Sleep((int)(m_timeout * 1000));
}
catch (ThreadAbortException)
{
// Thread.Abort() called from main thread. Ignore
return;
}
}
RenderStreaming.Logger.Log("Signaling: WS managing thread ended");
}
private void WSCreate()
{
m_webSocket = new WebSocket(m_url);
if (m_url.StartsWith("wss"))
{
m_webSocket.SslConfiguration.EnabledSslProtocols =
SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12;
}
m_webSocket.OnOpen += WSConnected;
m_webSocket.OnMessage += WSProcessMessage;
m_webSocket.OnError += WSError;
m_webSocket.OnClose += WSClosed;
Monitor.Enter(m_webSocket);
RenderStreaming.Logger.Log($"Signaling: Connecting WS {m_url}");
m_webSocket.ConnectAsync();
}
private void WSProcessMessage(object sender, MessageEventArgs e)
{
var content = Encoding.UTF8.GetString(e.RawData);
RenderStreaming.Logger.Log($"Signaling: Receiving message: {content}");
try
{
var routedMessage = JsonUtility.FromJson<RoutedMessage<SignalingMessage>>(content);
SignalingMessage msg;
if (!string.IsNullOrEmpty(routedMessage.type))
{
msg = routedMessage.data;
}
else
{
msg = JsonUtility.FromJson<SignalingMessage>(content);
}
if (!string.IsNullOrEmpty(routedMessage.type))
{
if (routedMessage.type == "connect")
{
msg = JsonUtility.FromJson<SignalingMessage>(content);
m_mainThreadContext.Post(d => OnCreateConnection?.Invoke(this, msg.connectionId, msg.polite), null);
participantId=msg.participantId;
}
else if (routedMessage.type == "disconnect")
{
msg = JsonUtility.FromJson<SignalingMessage>(content);
m_mainThreadContext.Post(d => OnDestroyConnection?.Invoke(this, msg.connectionId), null);
}
else if (routedMessage.type == "offer")
{
DescData offer = new DescData();
offer.connectionId = routedMessage.from;
offer.sdp = msg.sdp;
offer.polite = msg.polite;
m_mainThreadContext.Post(d => OnOffer?.Invoke(this, offer), null);
}
else if (routedMessage.type == "answer")
{
DescData answer = new DescData
{
connectionId = routedMessage.from,
sdp = msg.sdp
};
m_mainThreadContext.Post(d => OnAnswer?.Invoke(this, answer), null);
}
else if (routedMessage.type == "candidate")
{
CandidateData candidate = new CandidateData
{
connectionId = routedMessage.@from,
candidate = msg.candidate,
sdpMLineIndex = msg.sdpMLineIndex,
sdpMid = msg.sdpMid
};
m_mainThreadContext.Post(d => OnIceCandidate?.Invoke(this, candidate), null);
}
else if (routedMessage.type == "error")
{
msg = JsonUtility.FromJson<SignalingMessage>(content);
RenderStreaming.Logger.Log(LogType.Error, msg.message);
}
}
}
catch (Exception ex)
{
RenderStreaming.Logger.Log(LogType.Error, "Signaling: Failed to parse message: " + ex);
}
}
private void WSConnected(object sender, EventArgs e)
{
RenderStreaming.Logger.Log("Signaling: WS connected.");
m_mainThreadContext.Post(d => OnStart?.Invoke(this), null);
}
private void WSError(object sender, ErrorEventArgs e)
{
RenderStreaming.Logger.Log(LogType.Error, $"Signaling: WS connection error: {e.Message}");
}
private void WSClosed(object sender, CloseEventArgs e)
{
RenderStreaming.Logger.Log($"Signaling: WS connection closed, code: {e.Code}");
m_wsCloseEvent.Set();
m_webSocket = null;
}
private void WSSend(object data)
{
if (m_webSocket == null || m_webSocket.ReadyState != WebSocketState.Open)
{
RenderStreaming.Logger.Log(LogType.Error, "Signaling: WS is not connected. Unable to send message");
return;
}
if (data is string s)
{
RenderStreaming.Logger.Log("Signaling: Sending WS data: " + s);
m_webSocket.Send(s);
}
else
{
string str = JsonUtility.ToJson(data);
RenderStreaming.Logger.Log("Signaling: Sending WS data: " + str);
m_webSocket.Send(str);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ad8d529710094c2895227a94d10eb8a6
timeCreated: 1586218434

View File

@@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Unity.RenderStreaming.Signaling;
using UnityEngine;
namespace Unity.RenderStreaming
{
/// <summary>
///
/// </summary>
[Serializable, SignalingType("websocket")]
public class WebSocketSignalingSettings : SignalingSettings
{
/// <summary>
///
/// </summary>
public override Type signalingClass => typeof(WebSocketSignaling);
/// <summary>
///
/// </summary>
public override IReadOnlyCollection<IceServer> iceServers => m_iceServers;
/// <summary>
///
/// </summary>
public string url => m_url;
[SerializeField, Tooltip("Set the signaling server URL. you should specify a URL starting with \"ws\" or \"wss\".")]
protected string m_url;
[SerializeField, Tooltip("Set a list of STUN/TURN servers.")]
protected IceServer[] m_iceServers;
/// <summary>
///
/// </summary>
/// <param name="url"></param>
/// <param name="iceServers"></param>
public WebSocketSignalingSettings(string url, IceServer[] iceServers = null)
{
if (url == null)
throw new ArgumentNullException("url");
if (!Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute))
throw new ArgumentException("url is not well formed Uri");
m_url = url;
m_iceServers = iceServers == null ? Array.Empty<IceServer>() : iceServers.Select(server => server.Clone()).ToArray();
}
/// <summary>
///
/// </summary>
public WebSocketSignalingSettings()
{
m_url = "ws://127.0.0.1";
m_iceServers = new[]
{
new IceServer (urls: new[] {"stun:stun.l.google.com:19302"})
};
}
public override bool ParseArguments(string[] arguments)
{
if (arguments == null)
throw new ArgumentNullException("arguments");
if (arguments.Length == 0)
throw new ArgumentException("arguments is empty");
if (!CommandLineParser.TryParse(arguments))
return false;
if (CommandLineParser.ImportJson.Value != null)
{
CommandLineInfo info = CommandLineParser.ImportJson.Value.Value;
if (info.signalingUrl != null)
m_url = info.signalingUrl;
if (info.iceServers != null && info.iceServers.Length != 0)
m_iceServers = info.iceServers.Select(v => new IceServer(v)).ToArray();
}
if (CommandLineParser.SignalingUrl.Value != null)
m_url = CommandLineParser.SignalingUrl.Value;
var username = CommandLineParser.IceServerUsername != null
? CommandLineParser.IceServerUsername.Value
: null;
var credential = CommandLineParser.IceServerCredential != null
? CommandLineParser.IceServerCredential.Value
: null;
var credentialType = CommandLineParser.IceServerCredentialType != null
? CommandLineParser.IceServerCredentialType.Value
: null;
var urls = CommandLineParser.IceServerUrls != null
? CommandLineParser.IceServerUrls.Value
: null;
if (m_iceServers.Length > 0)
m_iceServers[0] = m_iceServers[0].Clone(
username: username,
credential: credential,
credentialType: credentialType,
urls: urls);
else
m_iceServers = new IceServer[]
{
new IceServer(
username: username,
credential: credential,
credentialType: credentialType.GetValueOrDefault(),
urls: urls)
};
return true;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bb4c44f4a1edb4d4a9a787ca2046cd1c
timeCreated: 1674112040

View File

@@ -0,0 +1,39 @@
using Unity.WebRTC;
using UnityEngine.EventSystems;
namespace Unity.RenderStreaming
{
/// <summary>
///
/// </summary>
public class SignalingEventData : BaseEventData
{
/// <summary>
///
/// </summary>
public string connectionId { get; set; }
/// <summary>
///
/// </summary>
public RTCDataChannel channel { get; set; }
/// <summary>
///
/// </summary>
public RTCRtpTransceiver transceiver { get; set; }
/// <summary>
///
/// </summary>
public string sdp { get; set; }
/// <summary>
///
/// </summary>
/// <param name="eventSystem"></param>
public SignalingEventData(EventSystem eventSystem) : base(eventSystem)
{
}
}
}

View File

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

View File

@@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using Unity.WebRTC;
using UnityEngine;
using UnityEngine.EventSystems;
namespace Unity.RenderStreaming
{
internal class SignalingEventProvider
{
private List<WeakReference<GameObject>> m_list = new List<WeakReference<GameObject>>();
public SignalingEventProvider(IRenderStreamingDelegate handler)
{
handler.onCreatedConnection += OnCreatedConnection;
handler.onDeletedConnection += OnDeletedConnection;
handler.onConnect += OnConnect;
handler.onDisconnect += OnDisconnect;
handler.onGotOffer += OnGotOffer;
handler.onGotAnswer += OnGotAnswer;
handler.onAddChannel += OnAddChannel;
handler.onAddTransceiver += OnAddReceiver;
}
public bool Subscribe(Component comp)
{
if (Find(comp.gameObject) != null)
return false;
m_list.Add(new WeakReference<GameObject>(comp.gameObject));
return true;
}
public bool Unsubscribe(Component comp)
{
var a = Find(comp.gameObject);
if (a == null)
return false;
m_list.Remove(a);
return true;
}
private WeakReference<GameObject> Find(GameObject obj)
{
return m_list.Find(r =>
{
if (!r.TryGetTarget(out var _obj))
return false;
return obj == _obj;
});
}
private void ExecuteEventToAllTargets<T>(
BaseEventData data, ExecuteEvents.EventFunction<T> functor)
where T : IEventSystemHandler
{
foreach (var r in m_list)
{
if (!r.TryGetTarget(out var obj))
continue;
ExecuteEvents.Execute(obj, data, functor);
}
}
private void OnCreatedConnection(string connectionId)
{
var data = new SignalingEventData(EventSystem.current)
{
connectionId = connectionId
};
ExecuteEventToAllTargets(data, ExecuteSignalingEvents.createdConnectionHandler);
}
private void OnDeletedConnection(string connectionId)
{
var data = new SignalingEventData(EventSystem.current)
{
connectionId = connectionId
};
ExecuteEventToAllTargets(data, ExecuteSignalingEvents.deletedConnectionHandler);
}
private void OnGotOffer(string connectionId, string sdp)
{
var data = new SignalingEventData(EventSystem.current)
{
connectionId = connectionId,
sdp = sdp
};
ExecuteEventToAllTargets(data, ExecuteSignalingEvents.offerHandler);
}
private void OnGotAnswer(string connectionId, string sdp)
{
var data = new SignalingEventData(EventSystem.current)
{
connectionId = connectionId,
sdp = sdp
};
ExecuteEventToAllTargets(data, ExecuteSignalingEvents.answerHandler);
}
private void OnConnect(string connectionId)
{
var data = new SignalingEventData(EventSystem.current)
{
connectionId = connectionId
};
ExecuteEventToAllTargets(data, ExecuteSignalingEvents.connectHandler);
}
private void OnDisconnect(string connectionId)
{
var data = new SignalingEventData(EventSystem.current)
{
connectionId = connectionId
};
ExecuteEventToAllTargets(data, ExecuteSignalingEvents.disconnectHandler);
}
private void OnAddChannel(string connectionId, RTCDataChannel channel)
{
var data = new SignalingEventData(EventSystem.current)
{
connectionId = connectionId,
channel = channel
};
ExecuteEventToAllTargets(data, ExecuteSignalingEvents.addChannelHandler);
}
private void OnAddReceiver(string connectionId, RTCRtpTransceiver transceiver)
{
var data = new SignalingEventData(EventSystem.current)
{
connectionId = connectionId,
transceiver = transceiver
};
ExecuteEventToAllTargets(data, ExecuteSignalingEvents.addReceiverHandler);
}
}
}

View File

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

View File

@@ -0,0 +1,397 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Unity.WebRTC;
using UnityEngine;
namespace Unity.RenderStreaming
{
/// <summary>
///
/// </summary>
public abstract class SignalingHandlerBase : MonoBehaviour
{
private IRenderStreamingHandler m_handler;
/// <summary>
///
/// </summary>
public virtual IEnumerable<Component> Streams => null;
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
public virtual void CreateConnection(string connectionId)
{
m_handler.CreateConnection(connectionId);
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
public virtual void DeleteConnection(string connectionId)
{
m_handler.DeleteConnection(connectionId);
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <returns></returns>
public virtual bool ExistConnection(string connectionId)
{
return m_handler.ExistConnection(connectionId);
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <returns></returns>
public virtual bool IsConnected(string connectionId)
{
return m_handler.IsConnected(connectionId);
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <returns></returns>
public virtual bool IsStable(string connectionId)
{
return m_handler.IsStable(connectionId);
}
static RTCRtpTransceiverInit GetTransceiverInit(IStreamSender sender)
{
RTCRtpTransceiverInit init = new RTCRtpTransceiverInit()
{
direction = RTCRtpTransceiverDirection.SendOnly,
};
if (sender is VideoStreamSender videoStreamSender)
{
init.sendEncodings = new RTCRtpEncodingParameters[]
{
new RTCRtpEncodingParameters()
{
active = true,
minBitrate = (ulong?)videoStreamSender.minBitrate * 1000,
maxBitrate = (ulong?)videoStreamSender.maxBitrate * 1000,
maxFramerate = (uint?)videoStreamSender.frameRate,
scaleResolutionDownBy = videoStreamSender.scaleResolutionDown
}
};
}
if (sender is AudioStreamSender audioStreamSender)
{
init.sendEncodings = new RTCRtpEncodingParameters[]
{
new RTCRtpEncodingParameters()
{
active = true,
minBitrate = (ulong?)audioStreamSender.minBitrate * 1000,
maxBitrate = (ulong?)audioStreamSender.maxBitrate * 1000,
}
};
}
return init;
}
internal static VideoCodecInfo[] GetVideoCodecInfo(IStreamSender sender)
{
if (sender is VideoStreamSender videoStreamSender)
{
if (videoStreamSender.codec == null)
return new VideoCodecInfo[] { };
return new VideoCodecInfo[] { videoStreamSender.codec };
}
throw new ArgumentException("sender is not for video streaming.", "sender");
}
internal static AudioCodecInfo[] GetAudioCodecInfo(IStreamSender sender)
{
if (sender is AudioStreamSender audioStreamSender)
{
if (audioStreamSender.codec == null)
return new AudioCodecInfo[] { };
return new AudioCodecInfo[] { audioStreamSender.codec };
}
throw new ArgumentException("sender is not for audio streaming.", "sender");
}
internal static VideoCodecInfo[] GetVideoCodecInfo(IStreamReceiver receiver)
{
if (receiver is VideoStreamReceiver videoStreamReceiver)
{
if (videoStreamReceiver.codec == null)
return new VideoCodecInfo[] { };
return new VideoCodecInfo[] { videoStreamReceiver.codec };
}
throw new ArgumentException("receiver is not for video streaming.", "receiver");
}
internal static AudioCodecInfo[] GetAudioCodecInfo(IStreamReceiver receiver)
{
if (receiver is AudioStreamReceiver audioStreamReceiver)
{
if (audioStreamReceiver.codec == null)
return new AudioCodecInfo[] { };
return new AudioCodecInfo[] { audioStreamReceiver.codec };
}
throw new ArgumentException("receiver is not for audio streaming.", "receiver");
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <param name="sender"></param>
/// <returns></returns>
public virtual void AddSender(string connectionId, IStreamSender sender)
{
StartCoroutine(AddSenderCoroutine(connectionId, sender));
}
private IEnumerator AddSenderCoroutine(string connectionId, IStreamSender sender)
{
if (sender.Track == null && sender is StreamSenderBase senderBase)
{
var op = senderBase.CreateTrack();
if (op.Track == null)
yield return op;
senderBase.SetTrack(op.Track);
}
if (sender.Track == null)
throw new InvalidOperationException("sender.Track is null");
RTCRtpTransceiverInit init = GetTransceiverInit(sender);
var transceiver = m_handler.AddTransceiver(connectionId, sender.Track, init);
if (sender is VideoStreamSender videoStreamSender)
{
var codecs = GetVideoCodecInfo(sender);
transceiver.SetCodecPreferences(videoStreamSender.SelectCodecCapabilities(codecs).ToArray());
}
else if (sender is AudioStreamSender audioStreamSender)
{
var codecs = GetAudioCodecInfo(sender);
transceiver.SetCodecPreferences(audioStreamSender.SelectCodecCapabilities(codecs).ToArray());
}
sender.SetTransceiver(connectionId, transceiver);
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <param name="sender"></param>
public virtual void RemoveSender(string connectionId, IStreamSender sender)
{
if (ExistConnection(connectionId))
RemoveTrack(connectionId, sender.Track);
sender.SetTransceiver(connectionId, null);
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <param name="receiver"></param>
/// <param name="transceiver"></param>
public virtual void SetReceiver(string connectionId, IStreamReceiver receiver, RTCRtpTransceiver transceiver)
{
if (receiver is VideoStreamReceiver videoStreamReceiver)
{
var codecs = GetVideoCodecInfo(receiver);
transceiver.SetCodecPreferences(videoStreamReceiver.SelectCodecCapabilities(codecs).ToArray());
}
else if (receiver is AudioStreamReceiver audioStreamReceiver)
{
var codecs = GetAudioCodecInfo(receiver);
transceiver.SetCodecPreferences(audioStreamReceiver.SelectCodecCapabilities(codecs).ToArray());
}
receiver.SetTransceiver(connectionId, transceiver);
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <param name="receiver"></param>
public virtual void RemoveReceiver(string connectionId, IStreamReceiver receiver)
{
receiver.SetTransceiver(connectionId, null);
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <param name="channel"></param>
public virtual void AddChannel(string connectionId, IDataChannel channel)
{
if (channel.IsLocal)
{
var _channel = m_handler.CreateChannel(connectionId, channel.Label);
channel.SetChannel(connectionId, _channel);
}
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <param name="channel"></param>
public virtual void RemoveChannel(string connectionId, IDataChannel channel)
{
channel.SetChannel(connectionId, null);
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <param name="track"></param>
protected virtual void RemoveTrack(string connectionId, MediaStreamTrack track)
{
m_handler.RemoveSenderTrack(connectionId, track);
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
public virtual void SendOffer(string connectionId)
{
m_handler.SendOffer(connectionId);
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
public virtual void SendAnswer(string connectionId)
{
m_handler.SendAnswer(connectionId);
}
internal void SetHandler(IRenderStreamingHandler handler)
{
m_handler = handler;
}
}
/// <summary>
///
/// </summary>
public delegate void OnStartedStreamHandler(string connectionId);
/// <summary>
///
/// </summary>
public delegate void OnStoppedStreamHandler(string connectionId);
/// <summary>
///
/// </summary>
public delegate void OnStartedChannelHandler(string connectionId);
/// <summary>
///
/// </summary>
public delegate void OnStoppedChannelHandler(string connectionId);
/// <summary>
///
/// </summary>
public interface IStreamSender
{
/// <summary>
///
/// </summary>
MediaStreamTrack Track { get; }
/// <summary>
///
/// </summary>
IReadOnlyDictionary<string, RTCRtpTransceiver> Transceivers { get; }
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <param name="transceiver"></param>
void SetTransceiver(string connectionId, RTCRtpTransceiver transceiver);
}
/// <summary>
///
/// </summary>
public interface IStreamReceiver
{
/// <summary>
///
/// </summary>
MediaStreamTrack Track { get; }
/// <summary>
///
/// </summary>
RTCRtpTransceiver Transceiver { get; }
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <param name="transceiver"></param>
void SetTransceiver(string connectionId, RTCRtpTransceiver transceiver);
}
/// <summary>
///
/// </summary>
public interface IDataChannel
{
/// <summary>
///
/// </summary>
bool IsLocal { get; }
/// <summary>
///
/// </summary>
bool IsConnected { get; }
/// <summary>
///
/// </summary>
string Label { get; }
/// <summary>
///
/// </summary>
string ConnectionId { get; }
/// <summary>
///
/// </summary>
RTCDataChannel Channel { get; }
///// <summary>
/////
///// </summary>
///// <param name="track"></param>
void SetChannel(string connectionId, RTCDataChannel channel);
/// <summary>
///
/// </summary>
/// <param name="data"></param>
void SetChannel(SignalingEventData data);
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2d41336fa3d46fd4180ea7aa15e89be7
timeCreated: 1606988906

View File

@@ -0,0 +1,351 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using Unity.RenderStreaming.Signaling;
using Unity.WebRTC;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Unity.RenderStreaming
{
/// <summary>
/// Manages the signaling process for Unity RenderStreaming.
/// </summary>
/// <seealso cref="ISignaling"/>
/// <seealso cref="SignalingSettings"/>
/// <seealso cref="SignalingHandlerBase"/>
[AddComponentMenu("Render Streaming/Signaling Manager")]
public sealed class SignalingManager : MonoBehaviour
{
internal const string UseDefaultPropertyName = nameof(m_useDefault);
internal const string SignalingSettingsObjectPropertyName = nameof(signalingSettingsObject);
internal const string SignalingSettingsPropertyName = nameof(signalingSettings);
internal const string HandlersPropertyName = nameof(handlers);
internal const string RunOnAwakePropertyName = nameof(runOnAwake);
internal const string EvaluateCommandlineArgumentsPropertyName = nameof(evaluateCommandlineArguments);
#pragma warning disable 0649
[SerializeField, Tooltip("Use settings in Project Settings Window.")]
private bool m_useDefault = true;
[SerializeField]
internal SignalingSettingsObject signalingSettingsObject;
[SerializeReference, SignalingSettings]
private SignalingSettings signalingSettings = new WebSocketSignalingSettings();
[SerializeField, Tooltip("List of handlers of signaling process.")]
private List<SignalingHandlerBase> handlers = new List<SignalingHandlerBase>();
/// <summary>
/// Indicates whether the signaling process should automatically start when the Awake method is called.
/// </summary>
[SerializeField, Tooltip("Automatically started when called Awake method.")]
public bool runOnAwake = true;
/// <summary>
/// Indicates whether to evaluate command line arguments if launching runtime on the command line.
/// </summary>
[SerializeField, Tooltip("Evaluate commandline arguments if launching runtime on the command line.")]
public bool evaluateCommandlineArguments = true;
#pragma warning restore 0649
private SignalingManagerInternal m_instance;
private SignalingEventProvider m_provider;
private bool m_running;
/// <summary>
/// Gets a value indicating whether the signaling process is running.
/// </summary>
public bool Running => m_running;
static ISignaling CreateSignaling(SignalingSettings settings, SynchronizationContext context)
{
if (settings.signalingClass == null)
{
throw new ArgumentException($"Signaling type is undefined. {settings.signalingClass}");
}
object[] args = { settings, context };
return (ISignaling)Activator.CreateInstance(settings.signalingClass, args);
}
/// <summary>
/// Use settings in Project Settings.
/// </summary>
public bool useDefaultSettings
{
get { return m_useDefault; }
set { m_useDefault = value; }
}
/// <summary>
/// Sets the signaling settings.
/// </summary>
/// <example>
/// <code>
/// var settings = new WebSocketSignalingSettings("ws://example.com", new[]
/// {
/// new IceServer (urls: new[] {"stun:stun.l.google.com:19302"})
/// });
/// signalingManager.SetSignalingSettings(settings);
///</code>
/// </example>
/// <param name="settings">The signaling settings.</param>
/// <exception cref="InvalidOperationException">Thrown when the signaling process has already started.</exception>
/// <exception cref="ArgumentNullException">Thrown when the settings are null.</exception>
public void SetSignalingSettings(SignalingSettings settings)
{
if (m_running)
throw new InvalidOperationException("The Signaling process has already started.");
if (settings == null)
throw new ArgumentNullException("settings");
signalingSettings = settings;
}
/// <summary>
/// Gets the signaling settings.
/// </summary>
/// <example>
/// <code>
/// var settings = signalingManager.GetSignalingSettings();
/// if (settings is WebSocketSignalingSettings webSocketSettings)
/// {
/// Debug.Log($"WebSocket URL: {webSocketSettings.url}");
/// }
///</code>
/// </example>
/// <returns>The signaling settings.</returns>
public SignalingSettings GetSignalingSettings()
{
return signalingSettings;
}
/// <summary>
/// Adds a signaling handler.
/// </summary>
/// <example>
/// <code>
/// var handler = instance.GetComponent<Multiplay>();
/// signalingManager.AddSignalingHandler(handler);
///</code>
/// </example>
/// <param name="handlerBase">The signaling handler to add.</param>
public void AddSignalingHandler(SignalingHandlerBase handlerBase)
{
if (handlers.Contains(handlerBase))
{
return;
}
handlers.Add(handlerBase);
if (!m_running)
{
return;
}
handlerBase.SetHandler(m_instance);
m_provider.Subscribe(handlerBase);
}
/// <summary>
/// Removes a signaling handler.
/// </summary>
/// <example>
/// <code>
/// var handler = instance.GetComponent<Multiplay>();
/// signalingManager.RemoveSignalingHandler(handler);
///</code>
/// </example>
/// <param name="handlerBase">The signaling handler to remove.</param>
public void RemoveSignalingHandler(SignalingHandlerBase handlerBase)
{
handlers.Remove(handlerBase);
if (!m_running)
{
return;
}
handlerBase.SetHandler(null);
m_provider.Unsubscribe(handlerBase);
}
/// <summary>
/// Runs the signaling process.
/// </summary>
/// <param name="signaling">The signaling instance to use. If null, a new instance will be created.</param>
/// <param name="handlers">The signaling handlers to use. If null, the existing handlers will be used.</param>
/// <example>
/// <code>
/// signalingManager.Run();
///</code>
/// </example>
public void Run(
ISignaling signaling = null,
SignalingHandlerBase[] handlers = null)
{
_Run(null, signaling, handlers);
}
/// <summary>
/// Runs the signaling process with the specified RTC configuration.
/// </summary>
/// <example>
/// <code>
/// var rtcConfig = new RTCConfiguration
/// {
/// iceServers = new[] { new RTCIceServer { urls = new[] { "stun:stun.l.google.com:19302" } } }
/// };
/// signalingManager.Run(rtcConfig);
///</code>
/// </example>
/// <param name="conf">The RTC configuration.</param>
/// <param name="signaling">The signaling instance to use. If null, a new instance will be created.</param>
/// <param name="handlers">The signaling handlers to use. If null, the existing handlers will be used.</param>
/// <remarks>To use this method, the WebRTC package is required.</remarks>
public void Run(
RTCConfiguration conf,
ISignaling signaling = null,
SignalingHandlerBase[] handlers = null
)
{
_Run(conf, signaling, handlers);
}
#if UNITY_EDITOR
bool IsValidSignalingSettingsObject(SignalingSettingsObject asset)
{
if (asset == null)
return false;
if (AssetDatabase.GetAssetPath(asset).IndexOf("Assets", StringComparison.Ordinal) != 0)
return false;
return true;
}
#endif
private void _Run(
RTCConfiguration? conf = null,
ISignaling signaling = null,
SignalingHandlerBase[] handlers = null
)
{
var settings = m_useDefault ? RenderStreaming.GetSignalingSettings<SignalingSettings>() : signalingSettings;
#if !UNITY_EDITOR
var arguments = Environment.GetCommandLineArgs();
if (evaluateCommandlineArguments && arguments.Length > 1)
{
if (!EvaluateCommandlineArguments(ref settings, arguments))
{
RenderStreaming.Logger.Log(LogType.Error, "Command line arguments are invalid.");
}
}
#endif
int i = 0;
RTCIceServer[] iceServers = new RTCIceServer[settings.iceServers.Count()];
foreach (var iceServer in settings.iceServers)
{
iceServers[i] = (RTCIceServer)iceServer;
i++;
}
RTCConfiguration _conf =
conf.GetValueOrDefault(new RTCConfiguration { iceServers = iceServers });
ISignaling _signaling = signaling ?? CreateSignaling(settings, SynchronizationContext.Current);
RenderStreamingDependencies dependencies = new RenderStreamingDependencies
{
config = _conf,
signaling = _signaling,
startCoroutine = StartCoroutine,
stopCoroutine = StopCoroutine,
resentOfferInterval = 5.0f,
};
var _handlers = (handlers ?? this.handlers.AsEnumerable()).Where(_ => _ != null);
if (_handlers.Count() == 0)
throw new InvalidOperationException("Handler list is empty.");
m_instance = new SignalingManagerInternal(ref dependencies);
m_provider = new SignalingEventProvider(m_instance);
foreach (var handler in _handlers)
{
handler.SetHandler(m_instance);
m_provider.Subscribe(handler);
}
m_running = true;
}
internal static bool EvaluateCommandlineArguments(ref SignalingSettings settings, string[] arguments)
{
if (!CommandLineParser.TryParse(arguments))
return false;
string signalingTypeName = null;
if (CommandLineParser.SignalingType.Value != null)
{
signalingTypeName = CommandLineParser.SignalingType;
}
else if (CommandLineParser.ImportJson.Value != null)
{
signalingTypeName = CommandLineParser.ImportJson.Value.Value.signalingType;
}
if (signalingTypeName != null)
{
Type[] types = RuntimeTypeCache<SignalingSettings>.GetTypesDerivedFrom();
Dictionary<string, Type> map =
types.Where(type => type.GetCustomAttribute<SignalingTypeAttribute>() != null)
.ToDictionary(type => type.GetCustomAttribute<SignalingTypeAttribute>().typename, type => type);
if (map.ContainsKey(signalingTypeName))
{
var type = map[signalingTypeName];
settings = (SignalingSettings)Activator.CreateInstance(type);
}
}
return settings.ParseArguments(arguments);
}
/// <summary>
/// Stops the signaling process.
/// </summary>
/// <example>
/// <code>
/// signalingManager.Stop();
///</code>
/// </example>
public void Stop()
{
m_instance?.Dispose();
m_instance = null;
m_running = false;
}
void Awake()
{
if (!runOnAwake || m_running || handlers.Count == 0)
return;
var settings = m_useDefault ? RenderStreaming.GetSignalingSettings<SignalingSettings>() : signalingSettings;
int i = 0;
RTCIceServer[] iceServers = new RTCIceServer[settings.iceServers.Count()];
foreach (var iceServer in settings.iceServers)
{
iceServers[i] = (RTCIceServer)iceServer;
i++;
}
RTCConfiguration conf = new RTCConfiguration { iceServers = iceServers };
ISignaling signaling = CreateSignaling(settings, SynchronizationContext.Current);
_Run(conf, signaling, handlers.ToArray());
}
void OnDestroy()
{
Stop();
}
}
}

View File

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

View File

@@ -0,0 +1,432 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Unity.RenderStreaming.Signaling;
using Unity.WebRTC;
using UnityEngine;
namespace Unity.RenderStreaming
{
/// <summary>
///
/// </summary>
internal struct RenderStreamingDependencies
{
/// <summary>
///
/// </summary>
public ISignaling signaling;
/// <summary>
///
/// </summary>
public RTCConfiguration config;
/// <summary>
///
/// </summary>
public Func<IEnumerator, Coroutine> startCoroutine;
/// <summary>
///
/// </summary>
public Action<Coroutine> stopCoroutine;
/// <summary>
/// unit is second;
/// </summary>
public float resentOfferInterval;
}
/// <summary>
///
/// </summary>
internal class SignalingManagerInternal : IDisposable,
IRenderStreamingHandler, IRenderStreamingDelegate
{
/// <summary>
///
/// </summary>
public event Action onStart;
/// <summary>
///
/// </summary>
public event Action<string> onCreatedConnection;
/// <summary>
///
/// </summary>
public event Action<string> onDeletedConnection;
/// <summary>
///
/// </summary>
public event Action<string, string> onGotOffer;
/// <summary>
///
/// </summary>
public event Action<string, string> onGotAnswer;
/// <summary>
///
/// </summary>
public event Action<string> onConnect;
/// <summary>
///
/// </summary>
public event Action<string> onDisconnect;
/// <summary>
///
/// </summary>
public event Action<string, RTCRtpTransceiver> onAddTransceiver;
/// <summary>
///
/// </summary>
public event Action<string, RTCDataChannel> onAddChannel;
private bool _disposed;
private readonly ISignaling _signaling;
private RTCConfiguration _config;
private readonly Func<IEnumerator, Coroutine> _startCoroutine;
private readonly Action<Coroutine> _stopCoroutine;
private readonly Dictionary<string, PeerConnection> _mapConnectionIdAndPeer =
new Dictionary<string, PeerConnection>();
private bool _runningResendCoroutine;
private float _resendInterval = 3.0f;
/// <summary>
///
/// </summary>
/// <param name="dependencies"></param>
public SignalingManagerInternal(ref RenderStreamingDependencies dependencies)
{
if (dependencies.signaling == null)
throw new ArgumentException("Signaling instance is null.");
if (dependencies.startCoroutine == null)
throw new ArgumentException("Coroutine action instance is null.");
_config = dependencies.config;
_startCoroutine = dependencies.startCoroutine;
_stopCoroutine = dependencies.stopCoroutine;
_resendInterval = dependencies.resentOfferInterval;
_signaling = dependencies.signaling;
_signaling.OnStart += OnStart;
_signaling.OnCreateConnection += OnCreateConnection;
_signaling.OnDestroyConnection += OnDestroyConnection;
_signaling.OnOffer += OnOffer;
_signaling.OnAnswer += OnAnswer;
_signaling.OnIceCandidate += OnIceCandidate;
_signaling.Start();
_startCoroutine(WebRTC.WebRTC.Update());
}
/// <summary>
///
/// </summary>
~SignalingManagerInternal()
{
Dispose();
}
/// <summary>
///
/// </summary>
public void Dispose()
{
if (this._disposed)
{
return;
}
_runningResendCoroutine = false;
_signaling.Stop();
_signaling.OnStart -= OnStart;
_signaling.OnCreateConnection -= OnCreateConnection;
_signaling.OnDestroyConnection -= OnDestroyConnection;
_signaling.OnOffer -= OnOffer;
_signaling.OnAnswer -= OnAnswer;
_signaling.OnIceCandidate -= OnIceCandidate;
foreach (var pair in _mapConnectionIdAndPeer)
pair.Value.Dispose();
this._disposed = true;
GC.SuppressFinalize(this);
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
public void CreateConnection(string connectionId)
{
_signaling.OpenConnection(connectionId);
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
public void DeleteConnection(string connectionId)
{
_signaling.CloseConnection(connectionId);
}
public bool ExistConnection(string connectionId)
{
return _mapConnectionIdAndPeer.ContainsKey(connectionId);
}
public bool IsConnected(string connectionId)
{
return _mapConnectionIdAndPeer.TryGetValue(connectionId, out var peer) && peer.IsConnected();
}
public bool IsStable(string connectionId)
{
return _mapConnectionIdAndPeer.TryGetValue(connectionId, out var peer) && peer.IsStable();
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <param name="track"></param>
public void RemoveSenderTrack(string connectionId, MediaStreamTrack track)
{
var sender = GetSenders(connectionId).First(s => s.Track == track);
_mapConnectionIdAndPeer[connectionId].peer.RemoveTrack(sender);
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <param name="track"></param>
/// <param name="direction"></param>
/// <returns></returns>
public RTCRtpTransceiver AddTransceiver(string connectionId, MediaStreamTrack track, RTCRtpTransceiverInit init = null)
{
var transceiver = _mapConnectionIdAndPeer[connectionId].peer.AddTransceiver(track, init);
return transceiver;
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <param name="kind"></param>
/// <param name="direction"></param>
/// <returns></returns>
public RTCRtpTransceiver AddTransceiver(string connectionId, TrackKind kind, RTCRtpTransceiverInit init = null)
{
var transceiver = _mapConnectionIdAndPeer[connectionId].peer.AddTransceiver(kind, init);
return transceiver;
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <param name="name"></param>
/// <returns></returns>
public RTCDataChannel CreateChannel(string connectionId, string name)
{
RTCDataChannelInit conf = new RTCDataChannelInit();
if (string.IsNullOrEmpty(name))
name = Guid.NewGuid().ToString();
return _mapConnectionIdAndPeer[connectionId].peer.CreateDataChannel(name, conf);
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <param name="track"></param>
/// <returns></returns>
public IEnumerable<RTCRtpSender> GetSenders(string connectionId)
{
return _mapConnectionIdAndPeer[connectionId].peer.GetSenders();
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <param name="track"></param>
/// <returns></returns>
public IEnumerable<RTCRtpReceiver> GetReceivers(string connectionId)
{
return _mapConnectionIdAndPeer[connectionId].peer.GetReceivers();
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <param name="track"></param>
/// <returns></returns>
public IEnumerable<RTCRtpTransceiver> GetTransceivers(string connectionId)
{
return _mapConnectionIdAndPeer[connectionId].peer.GetTransceivers();
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
public void SendOffer(string connectionId)
{
if (!_mapConnectionIdAndPeer.TryGetValue(connectionId, out var pc))
return;
pc.SendOffer();
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
public void SendAnswer(string connectionId)
{
if (!_mapConnectionIdAndPeer.TryGetValue(connectionId, out var pc))
return;
pc.SendAnswer();
}
IEnumerator ResendOfferCoroutine()
{
HashSet<string> failedConnections = new HashSet<string>();
while (_runningResendCoroutine)
{
failedConnections.Clear();
foreach (var peer in _mapConnectionIdAndPeer)
{
if (peer.Value.peer.ConnectionState == RTCPeerConnectionState.Failed)
{
failedConnections.Add(peer.Key);
}
else if (peer.Value.waitingAnswer)
{
peer.Value.SendOffer();
}
}
foreach (var connectionId in failedConnections)
{
DestroyConnection(connectionId);
}
yield return 0;
}
}
void OnStart(ISignaling signaling)
{
if (!_runningResendCoroutine)
{
_runningResendCoroutine = true;
_startCoroutine(ResendOfferCoroutine());
}
onStart?.Invoke();
}
void OnCreateConnection(ISignaling signaling, string connectionId, bool polite)
{
CreatePeerConnection(connectionId, polite);
onCreatedConnection?.Invoke(connectionId);
}
void OnDestroyConnection(ISignaling signaling, string connectionId)
{
DestroyConnection(connectionId);
}
void DestroyConnection(string connectionId)
{
DeletePeerConnection(connectionId);
onDeletedConnection?.Invoke(connectionId);
}
PeerConnection CreatePeerConnection(string connectionId, bool polite)
{
if (_mapConnectionIdAndPeer.TryGetValue(connectionId, out var peer))
{
peer.Dispose();
}
peer = new PeerConnection(polite, _config, _resendInterval, _startCoroutine, _stopCoroutine);
_mapConnectionIdAndPeer[connectionId] = peer;
peer.OnConnectHandler += () => onConnect?.Invoke(connectionId);
peer.OnDisconnectHandler += () =>
{
_signaling?.CloseConnection(connectionId);
onDisconnect?.Invoke(connectionId);
};
peer.OnDataChannelHandler += channel => onAddChannel?.Invoke(connectionId, channel); ;
peer.OnTrackEventHandler += e => onAddTransceiver?.Invoke(connectionId, e.Transceiver);
peer.SendOfferHandler += desc => _signaling?.SendOffer(connectionId, desc);
peer.SendAnswerHandler += desc => _signaling?.SendAnswer(connectionId, desc);
peer.SendCandidateHandler += candidate => _signaling?.SendCandidate(connectionId, candidate);
return peer;
}
void DeletePeerConnection(string connectionId)
{
if (!_mapConnectionIdAndPeer.TryGetValue(connectionId, out var peer))
{
return;
}
peer.Dispose();
_mapConnectionIdAndPeer.Remove(connectionId);
}
void OnAnswer(ISignaling signaling, DescData e)
{
if (!_mapConnectionIdAndPeer.TryGetValue(e.connectionId, out var pc))
{
RenderStreaming.Logger.Log(LogType.Warning, $"connectionId:{e.connectionId}, peerConnection not exist");
return;
}
RTCSessionDescription description = new RTCSessionDescription { type = RTCSdpType.Answer, sdp = e.sdp };
_startCoroutine(pc.OnGotDescription(description, () => onGotAnswer?.Invoke(e.connectionId, e.sdp)));
}
void OnIceCandidate(ISignaling signaling, CandidateData e)
{
if (!_mapConnectionIdAndPeer.TryGetValue(e.connectionId, out var pc))
{
return;
}
RTCIceCandidateInit option = new RTCIceCandidateInit
{
candidate = e.candidate,
sdpMLineIndex = e.sdpMLineIndex,
sdpMid = e.sdpMid
};
pc.OnGotIceCandidate(new RTCIceCandidate(option));
}
void OnOffer(ISignaling signaling, DescData e)
{
var connectionId = e.connectionId;
if (!_mapConnectionIdAndPeer.TryGetValue(connectionId, out var pc))
{
pc = CreatePeerConnection(connectionId, e.polite);
}
RTCSessionDescription description = new RTCSessionDescription { type = RTCSdpType.Offer, sdp = e.sdp };
_startCoroutine(pc.OnGotDescription(description, () => onGotOffer?.Invoke(connectionId, e.sdp)));
}
}
}

View File

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

View File

@@ -0,0 +1,117 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace Unity.RenderStreaming
{
public class SingleConnection : SignalingHandlerBase,
ICreatedConnectionHandler, IDeletedConnectionHandler,
IAddReceiverHandler, IOfferHandler, IAddChannelHandler
{
[SerializeField] private List<Component> streams = new List<Component>();
private string connectionId;
public override IEnumerable<Component> Streams => streams;
public void AddComponent(Component component)
{
streams.Add(component);
}
public void RemoveComponent(Component component)
{
streams.Remove(component);
}
public override void CreateConnection(string connectionId)
{
this.connectionId = connectionId;
base.CreateConnection(connectionId);
}
public override void DeleteConnection(string connectionId)
{
if (this.connectionId != connectionId)
return;
Disconnect(connectionId);
base.DeleteConnection(connectionId);
this.connectionId = null;
}
public void OnCreatedConnection(SignalingEventData data)
{
if (data.connectionId != connectionId)
return;
foreach (var sender in streams.OfType<IStreamSender>())
{
AddSender(data.connectionId, sender);
}
foreach (var channel in streams.OfType<IDataChannel>().Where(c => c.IsLocal))
{
AddChannel(connectionId, channel);
}
}
public void OnDeletedConnection(SignalingEventData data)
{
if (data.connectionId != connectionId)
return;
Disconnect(connectionId);
connectionId = null;
}
private void Disconnect(string connectionId)
{
foreach (var sender in streams.OfType<IStreamSender>())
{
RemoveSender(connectionId, sender);
}
foreach (var receiver in streams.OfType<IStreamReceiver>())
{
RemoveReceiver(connectionId, receiver);
}
foreach (var channel in streams.OfType<IDataChannel>())
{
RemoveChannel(connectionId, channel);
}
}
public void OnOffer(SignalingEventData data)
{
if (data.connectionId != connectionId)
return;
SendAnswer(data.connectionId);
}
public void OnAddReceiver(SignalingEventData data)
{
if (data.connectionId != connectionId)
return;
var track = data.transceiver.Receiver.Track;
IStreamReceiver receiver = GetReceiver(track.Kind);
SetReceiver(data.connectionId, receiver, data.transceiver);
}
public void OnAddChannel(SignalingEventData data)
{
if (data.connectionId != connectionId)
return;
var channel = streams.OfType<IDataChannel>().FirstOrDefault(r => !r.IsConnected && !r.IsLocal);
channel?.SetChannel(connectionId, data.channel);
}
IStreamReceiver GetReceiver(WebRTC.TrackKind kind)
{
if (kind == WebRTC.TrackKind.Audio)
return streams.OfType<AudioStreamReceiver>().First();
if (kind == WebRTC.TrackKind.Video)
return streams.OfType<VideoStreamReceiver>().First();
throw new System.ArgumentException();
}
}
}

View File

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

View File

@@ -0,0 +1,75 @@
using System;
using Unity.WebRTC;
using UnityEngine;
namespace Unity.RenderStreaming
{
/// <summary>
///
/// </summary>
public abstract class StreamReceiverBase : MonoBehaviour, IStreamReceiver
{
/// <summary>
///
/// </summary>
public RTCRtpTransceiver Transceiver => m_transceiver;
/// <summary>
///
/// </summary>
public OnStartedStreamHandler OnStartedStream { get; set; }
/// <summary>
///
/// </summary>
public OnStoppedStreamHandler OnStoppedStream { get; set; }
/// <summary>
///
/// </summary>
public MediaStreamTrack Track => m_track;
/// <summary>
///
/// </summary>
public bool isPlaying
{
get
{
if (string.IsNullOrEmpty(Transceiver.Mid))
return false;
if (Transceiver.Sender.Track.ReadyState == TrackState.Ended)
return false;
return true;
}
}
private RTCRtpTransceiver m_transceiver;
private MediaStreamTrack m_track;
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <param name="receiver"></param>
public virtual void SetTransceiver(string connectionId, RTCRtpTransceiver transceiver)
{
if (connectionId == null)
throw new ArgumentNullException("connectionId", "connectionId is null");
m_transceiver = transceiver;
m_track = m_transceiver?.Receiver.Track;
if (m_transceiver == null)
OnStoppedStream?.Invoke(connectionId);
else
OnStartedStream?.Invoke(connectionId);
}
protected virtual void OnDestroy()
{
m_track?.Dispose();
m_track = null;
}
}
}

View File

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

View File

@@ -0,0 +1,176 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Unity.WebRTC;
using UnityEngine;
namespace Unity.RenderStreaming
{
/// <summary>
///
/// </summary>
public abstract class StreamSenderBase : MonoBehaviour, IStreamSender
{
internal class WaitForCreateTrack : CustomYieldInstruction
{
public MediaStreamTrack Track { get { return m_track; } }
MediaStreamTrack m_track;
bool m_keepWaiting = true;
public override bool keepWaiting { get { return m_keepWaiting; } }
public WaitForCreateTrack() { }
public void Done(MediaStreamTrack track)
{
m_track = track;
m_keepWaiting = false;
}
}
internal Coroutine StartCoroutineWithCallback<T>(T coroutine, Action<T> callback) where T : IEnumerator
{
if (coroutine == null)
throw new ArgumentNullException("coroutine");
if (callback == null)
throw new ArgumentNullException("callback");
return StartCoroutine(_Coroutine(coroutine, callback));
}
internal IEnumerator _Coroutine<T>(T coroutine, Action<T> callback) where T : IEnumerator
{
yield return StartCoroutine(coroutine);
callback(coroutine);
}
/// <summary>
///
/// </summary>
public IReadOnlyDictionary<string, RTCRtpTransceiver> Transceivers => m_transceivers;
/// <summary>
///
/// </summary>
public OnStartedStreamHandler OnStartedStream { get; set; }
/// <summary>
///
/// </summary>
public OnStoppedStreamHandler OnStoppedStream { get; set; }
/// <summary>
///
/// </summary>
/// <returns></returns>
internal abstract WaitForCreateTrack CreateTrack();
internal virtual void ReplaceTrack(MediaStreamTrack newTrack)
{
if (newTrack == null)
throw new ArgumentNullException("track", "This argument must be not null.");
if (m_track == newTrack)
throw new ArgumentException("track", "The value of this argument has already been set.");
/// todo:: If not disposing the old track here, the app will crash.
/// This problem is caused by the MediaStreamTrack when it is destroyed on the thread other than the main thread.
m_track?.Dispose();
m_track = newTrack;
foreach (var transceiver in Transceivers.Values)
{
transceiver.Sender.ReplaceTrack(m_track);
}
}
internal void SetTrack(MediaStreamTrack newTrack)
{
if (newTrack == null)
throw new ArgumentNullException("track", "This argument must be not null.");
if (m_track != null)
throw new InvalidOperationException("Track is not null. Use ReplaceTrack method.");
m_track = newTrack;
}
private MediaStreamTrack m_track;
private Dictionary<string, RTCRtpTransceiver> m_transceivers =
new Dictionary<string, RTCRtpTransceiver>();
/// <summary>
///
/// </summary>
public MediaStreamTrack Track => m_track;
/// <summary>
///
/// </summary>
public bool isPlaying
{
get
{
foreach (var transceiver in Transceivers.Values)
{
if (string.IsNullOrEmpty(transceiver.Mid))
continue;
if (transceiver.Sender.Track.ReadyState == TrackState.Ended)
continue;
return true;
}
return false;
}
}
private protected virtual void OnDestroy()
{
m_track?.Dispose();
m_track = null;
}
private protected virtual void OnEnable()
{
if (m_track?.ReadyState == TrackState.Live)
{
m_track.Enabled = true;
}
}
private protected virtual void OnDisable()
{
if (m_track?.ReadyState == TrackState.Live)
{
m_track.Enabled = false;
}
}
/// <summary>
///
/// </summary>
/// <param name="connectionId"></param>
/// <param name="sender"></param>
public virtual void SetTransceiver(string connectionId, RTCRtpTransceiver transceiver)
{
if (connectionId == null)
throw new ArgumentNullException("connectionId is null");
if (transceiver == null)
{
m_transceivers.Remove(connectionId);
OnStoppedStream?.Invoke(connectionId);
if (!m_transceivers.Any())
{
m_track.Dispose();
m_track = null;
}
}
else
{
m_transceivers.Add(connectionId, transceiver);
OnStartedStream?.Invoke(connectionId);
}
}
}
}

View File

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

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