diff --git a/Assets/Samples/Stream Video & Audio Chat SDK/0.8.11.meta b/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20.meta similarity index 77% rename from Assets/Samples/Stream Video & Audio Chat SDK/0.8.11.meta rename to Assets/Samples/Stream Video & Audio Chat SDK/0.8.20.meta index 4268898e..2b364dff 100644 --- a/Assets/Samples/Stream Video & Audio Chat SDK/0.8.11.meta +++ b/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: b158ffcf61bc6174da296a0343e1061a +guid: 824737c426eb4344c877fa48581b6894 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/Samples/Stream Video & Audio Chat SDK/0.8.18.meta b/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20/Video & Audio Chat Example Project/Images/UI/Icons.meta similarity index 77% rename from Assets/Samples/Stream Video & Audio Chat SDK/0.8.18.meta rename to Assets/Samples/Stream Video & Audio Chat SDK/0.8.20/Video & Audio Chat Example Project/Images/UI/Icons.meta index d252a5fd..8bae00fc 100644 --- a/Assets/Samples/Stream Video & Audio Chat SDK/0.8.18.meta +++ b/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20/Video & Audio Chat Example Project/Images/UI/Icons.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 0e877e983999bbc42a9e80e7b68900b2 +guid: 134e2b75f899f0446ad0bf307cf6b05e folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20/Video & Audio Chat Example Project/Images/UI/Icons/mute_12226000.png b/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20/Video & Audio Chat Example Project/Images/UI/Icons/mute_12226000.png new file mode 100644 index 00000000..39817b4e Binary files /dev/null and b/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20/Video & Audio Chat Example Project/Images/UI/Icons/mute_12226000.png differ diff --git a/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20/Video & Audio Chat Example Project/Images/UI/Icons/mute_12226000.png.meta b/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20/Video & Audio Chat Example Project/Images/UI/Icons/mute_12226000.png.meta new file mode 100644 index 00000000..10892215 --- /dev/null +++ b/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20/Video & Audio Chat Example Project/Images/UI/Icons/mute_12226000.png.meta @@ -0,0 +1,140 @@ +fileFormatVersion: 2 +guid: b2cf530f9ab71904b9b9fecd4b1e4e36 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Windows Store Apps + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20/Video & Audio Chat Example Project/Prefabs/UI/Screens/ParticipantView.prefab b/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20/Video & Audio Chat Example Project/Prefabs/UI/Screens/ParticipantView.prefab index 883dee88..9e1f2672 100644 --- a/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20/Video & Audio Chat Example Project/Prefabs/UI/Screens/ParticipantView.prefab +++ b/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20/Video & Audio Chat Example Project/Prefabs/UI/Screens/ParticipantView.prefab @@ -32,8 +32,9 @@ RectTransform: m_ConstrainProportionsScale: 0 m_Children: - {fileID: 7252032941131618416} + - {fileID: 7637150640667577125} + - {fileID: 3604744137779619309} m_Father: {fileID: 2006121863544806348} - m_RootOrder: 1 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 0, y: 0} @@ -62,7 +63,7 @@ MonoBehaviour: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0, g: 0, b: 0, a: 1} - m_RaycastTarget: 1 + m_RaycastTarget: 0 m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: @@ -122,7 +123,6 @@ RectTransform: - {fileID: 2006121864280196913} - {fileID: 2006121863080428319} m_Father: {fileID: 0} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 0, y: 0} @@ -188,6 +188,11 @@ MonoBehaviour: _defaultSpeakerFrameColor: serializedVersion: 2 rgba: 0 + _forceRequestedResolution: 0 + _forceRequestedResolutionWidth: 300 + _forceRequestedResolutionHeight: 300 + _isMutedIcon: {fileID: 7852245601941594288} + _muteLocallyToggleButton: {fileID: 6073022831238027141} --- !u!1 &2006121864280196914 GameObject: m_ObjectHideFlags: 0 @@ -220,7 +225,6 @@ RectTransform: m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 2006121863544806348} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 1} m_AnchorMax: {x: 1, y: 1} @@ -344,6 +348,126 @@ MonoBehaviour: m_FlexibleWidth: -1 m_FlexibleHeight: -1 m_LayoutPriority: 1 +--- !u!1 &5510979313677697257 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3604744137779619309} + - component: {fileID: 6166759739252713308} + - component: {fileID: 6857646120311013141} + - component: {fileID: 6073022831238027141} + m_Layer: 5 + m_Name: MuteButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3604744137779619309 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5510979313677697257} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 2006121863080428319} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &6166759739252713308 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5510979313677697257} + m_CullTransparentMesh: 1 +--- !u!114 &6857646120311013141 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5510979313677697257} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!114 &6073022831238027141 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5510979313677697257} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 6857646120311013141} + m_OnClick: + m_PersistentCalls: + m_Calls: [] --- !u!1 &7624386267670761082 GameObject: m_ObjectHideFlags: 0 @@ -375,7 +499,6 @@ RectTransform: m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 2006121863080428319} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 1, y: 1} @@ -404,7 +527,7 @@ MonoBehaviour: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} - m_RaycastTarget: 1 + m_RaycastTarget: 0 m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: @@ -417,3 +540,99 @@ MonoBehaviour: y: 0 width: 1 height: 1 +--- !u!1 &7852245601941594288 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7637150640667577125} + - component: {fileID: 491838988929794835} + - component: {fileID: 8250186623793420590} + - component: {fileID: 7306340483036908546} + m_Layer: 5 + m_Name: IsMutedIcon + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!224 &7637150640667577125 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7852245601941594288} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 2006121863080428319} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: -10, y: 10} + m_SizeDelta: {x: 100, y: 100} + m_Pivot: {x: 1, y: 0} +--- !u!222 &491838988929794835 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7852245601941594288} + m_CullTransparentMesh: 1 +--- !u!114 &8250186623793420590 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7852245601941594288} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 21300000, guid: b2cf530f9ab71904b9b9fecd4b1e4e36, type: 3} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!114 &7306340483036908546 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7852245601941594288} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreLayout: 1 + m_MinWidth: -1 + m_MinHeight: -1 + m_PreferredWidth: -1 + m_PreferredHeight: -1 + m_FlexibleWidth: -1 + m_FlexibleHeight: -1 + m_LayoutPriority: 1 diff --git a/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20/Video & Audio Chat Example Project/Scripts/StreamVideoManager.cs b/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20/Video & Audio Chat Example Project/Scripts/StreamVideoManager.cs index c92ec2a1..2f402aee 100644 --- a/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20/Video & Audio Chat Example Project/Scripts/StreamVideoManager.cs +++ b/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20/Video & Audio Chat Example Project/Scripts/StreamVideoManager.cs @@ -1,10 +1,12 @@ using System; +using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; using StreamVideo.Core; using StreamVideo.Core.Configs; using StreamVideo.Core.Exceptions; using StreamVideo.Core.StatefulModels; +using StreamVideo.Core.StatefulModels.Tracks; using StreamVideo.Libs.Auth; using StreamVideo.Libs.Serialization; using StreamVideo.Libs.Utils; @@ -140,6 +142,33 @@ public void ToggleMusic(bool loop = true, bool forceStop = false) /// public void SetAudioREDundancyEncoding(bool value) => _clientConfig.Audio.EnableRed = value; + public void MuteLocally(IStreamVideoCallParticipant participant) + { + _isUserMutedLocally[participant.UserId] = true; + + if (participant.AudioTrack != null && participant.AudioTrack is StreamAudioTrack streamAudioTrack) + { + streamAudioTrack.MuteLocally(); + } + } + + public void UnmuteLocally(IStreamVideoCallParticipant participant) + { + if (!_isUserMutedLocally.ContainsKey(participant.UserId)) + { + return; + } + _isUserMutedLocally.Remove(participant.UserId); + + if (participant.AudioTrack != null && participant.AudioTrack is StreamAudioTrack streamAudioTrack) + { + streamAudioTrack.UnmuteLocally(); + } + } + + public bool IsParticipantMutedLocally(IStreamVideoCallParticipant participant) + => _isUserMutedLocally.ContainsKey(participant.UserId) && _isUserMutedLocally[participant.UserId]; + protected async void Start() { var credentials = new AuthCredentials(_apiKey, _userId, _userToken); @@ -271,6 +300,9 @@ private string _info private bool _wasAudioPublishEnabledOnPause; private bool _wasVideoPublishEnabledOnPause; + // We mute by user ID because Session ID will change every time the user reconnects + private readonly Dictionary _isUserMutedLocally = new Dictionary(); + private async Task ConnectToStreamAsync(AuthCredentials credentials) { var credentialsEmpty = string.IsNullOrEmpty(credentials.ApiKey) && diff --git a/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20/Video & Audio Chat Example Project/Scripts/UI/ParticipantView.cs b/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20/Video & Audio Chat Example Project/Scripts/UI/ParticipantView.cs index 0ea16f99..50462f31 100644 --- a/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20/Video & Audio Chat Example Project/Scripts/UI/ParticipantView.cs +++ b/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20/Video & Audio Chat Example Project/Scripts/UI/ParticipantView.cs @@ -12,8 +12,10 @@ public class ParticipantView : MonoBehaviour { public IStreamVideoCallParticipant Participant { get; private set; } - public void Init(IStreamVideoCallParticipant participant) + public void Init(IStreamVideoCallParticipant participant, StreamVideoManager videoManager) { + _videoManager = videoManager ?? throw new ArgumentNullException(nameof(videoManager)); + if (Participant != null) { throw new NotSupportedException("reusing participant view for new participant is not supported yet"); @@ -57,6 +59,7 @@ protected void Awake() { _videoRectTransform = _video.GetComponent(); _baseVideoRotation = _videoRectTransform.rotation; + _muteLocallyToggleButton.onClick.AddListener(OnMuteLocallyToggleClicked); } // Called by Unity Engine @@ -131,10 +134,17 @@ protected void OnDestroy() [SerializeField] private int _forceRequestedResolutionHeight = 300; + [SerializeField] + private GameObject _isMutedIcon; + + [SerializeField] + private Button _muteLocallyToggleButton; + private AudioSource _audioSource; private RectTransform _videoRectTransform; private Vector2 _lastRequestedResolution; private Quaternion _baseVideoRotation; + private StreamVideoManager _videoManager; private void OnParticipantTrackAdded(IStreamVideoCallParticipant participant, IStreamTrack track) { @@ -152,6 +162,14 @@ private void OnParticipantTrackAdded(IStreamVideoCallParticipant participant, IS _audioSource = gameObject.AddComponent(); streamAudioTrack.SetAudioSourceTarget(_audioSource); + + // Apply cached local mute state in case participant rejoined + if (_videoManager.IsParticipantMutedLocally(participant)) + { + streamAudioTrack.MuteLocally(); + UpdateMuteIcon(); + } + break; case StreamVideoTrack streamVideoTrack: @@ -162,5 +180,36 @@ private void OnParticipantTrackAdded(IStreamVideoCallParticipant participant, IS throw new ArgumentOutOfRangeException(nameof(track)); } } + + private void OnMuteLocallyToggleClicked() + { + if (Participant == null) + { + return; + } + + var isMuted = _videoManager.IsParticipantMutedLocally(Participant); + var newIsMuted = !isMuted; + + var actionLabel = newIsMuted ? "Muted" : "Unmuted"; + Debug.Log(actionLabel + " participant with user ID: " + Participant.UserId); + + if (newIsMuted) + { + _videoManager.MuteLocally(Participant); + } + else + { + _videoManager.UnmuteLocally(Participant); + } + + UpdateMuteIcon(); + } + + private void UpdateMuteIcon() + { + var isMuted = _videoManager.IsParticipantMutedLocally(Participant); + _isMutedIcon.SetActive(isMuted); + } } } \ No newline at end of file diff --git a/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20/Video & Audio Chat Example Project/Scripts/UI/Screens/CallScreenView.cs b/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20/Video & Audio Chat Example Project/Scripts/UI/Screens/CallScreenView.cs index 4359c1ae..23844db6 100644 --- a/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20/Video & Audio Chat Example Project/Scripts/UI/Screens/CallScreenView.cs +++ b/Assets/Samples/Stream Video & Audio Chat SDK/0.8.20/Video & Audio Chat Example Project/Scripts/UI/Screens/CallScreenView.cs @@ -198,12 +198,12 @@ private void AddParticipant(IStreamVideoCallParticipant participant, bool sortPa { var parent = GetParticipantViewParent(participant); var view = Instantiate(_participantViewPrefab, parent); - view.Init(participant); + view.Init(participant, VideoManager); _participantSessionIdToView.Add(participant.SessionId, view); if (participant.IsLocalParticipant) { - // Set input camera as a video source for local participant - we won't receive OnTrack event for local participant + // Set input camera as a video source for local participant - we won't receive TrackAdded event for local participant var webCamTexture = VideoManager.Client.VideoDeviceManager.GetSelectedDeviceWebCamTexture(); view.SetLocalCameraSource(webCamTexture); //StreamTodo: this will invalidate each time WebCamTexture is internally replaced so we need a better way to expose this diff --git a/Packages/StreamVideo/Runtime/Core/StatefulModels/Tracks/StreamAudioTrack.cs b/Packages/StreamVideo/Runtime/Core/StatefulModels/Tracks/StreamAudioTrack.cs index 43e61c10..170b6632 100644 --- a/Packages/StreamVideo/Runtime/Core/StatefulModels/Tracks/StreamAudioTrack.cs +++ b/Packages/StreamVideo/Runtime/Core/StatefulModels/Tracks/StreamAudioTrack.cs @@ -13,6 +13,81 @@ public StreamAudioTrack(AudioStreamTrack track) { } + + /// + /// Mutes this audio track locally without affecting other users. + /// + /// + /// `MuteLocally` mutes this audio track on the local device only. + /// Other users in the call will not be affected. This is useful for temporarily + /// stopping playback of incoming audio without disconnecting the track. + /// This method is only available on Android platform. + /// + /// + /// + /// + /// + /// + public void MuteLocally() + { +#if UNITY_ANDROID && !UNITY_EDITOR + Track.MuteLocally(); +#else + throw new NotSupportedException($"The {nameof(MuteLocally)} method is currently only supported on Android platform."); +#endif + } + + /// + /// Unmutes this audio track locally. + /// + /// + /// `UnmuteLocally` unmutes a previously muted audio track on the local device. + /// This method is only available on Android platform. + /// + /// + /// + /// + /// + /// + public void UnmuteLocally() + { +#if UNITY_ANDROID && !UNITY_EDITOR + Track.UnmuteLocally(); +#else + throw new NotSupportedException($"The {nameof(UnmuteLocally)} method is currently only supported on Android platform."); +#endif + } + + /// + /// Checks if this audio track is locally muted. + /// + /// + /// `IsLocallyMuted` returns true if the track is currently muted on the local device. + /// This method is only available on Android platform. + /// + /// True if the track is locally muted, false otherwise. + /// + /// + /// + /// + /// + public bool IsLocallyMuted() + { +#if UNITY_ANDROID && !UNITY_EDITOR + return Track.IsLocallyMuted(); +#else + throw new NotSupportedException($"The {nameof(IsLocallyMuted)} method is currently only supported on Android platform."); +#endif + } public void SetAudioSourceTarget(AudioSource audioSource) { diff --git a/Packages/StreamVideo/Runtime/Libs/io.stream.unity.webrtc/Runtime/Plugins/Android/libwebrtc.aar b/Packages/StreamVideo/Runtime/Libs/io.stream.unity.webrtc/Runtime/Plugins/Android/libwebrtc.aar index a44ddb01..a9708701 100644 Binary files a/Packages/StreamVideo/Runtime/Libs/io.stream.unity.webrtc/Runtime/Plugins/Android/libwebrtc.aar and b/Packages/StreamVideo/Runtime/Libs/io.stream.unity.webrtc/Runtime/Plugins/Android/libwebrtc.aar differ diff --git a/Packages/StreamVideo/Runtime/Libs/io.stream.unity.webrtc/Runtime/Scripts/AudioStreamTrack.cs b/Packages/StreamVideo/Runtime/Libs/io.stream.unity.webrtc/Runtime/Scripts/AudioStreamTrack.cs index c8d09ac4..8d568c6d 100644 --- a/Packages/StreamVideo/Runtime/Libs/io.stream.unity.webrtc/Runtime/Scripts/AudioStreamTrack.cs +++ b/Packages/StreamVideo/Runtime/Libs/io.stream.unity.webrtc/Runtime/Scripts/AudioStreamTrack.cs @@ -290,6 +290,80 @@ internal void RemoveSink(AudioStreamRenderer renderer) GetSelfOrThrow(), renderer.self); } +#if UNITY_ANDROID && !UNITY_EDITOR + /// + /// Mutes this audio track locally without affecting other users. + /// + /// + /// `MuteLocally` mutes this audio track on the local device only. + /// Other users in the call will not be affected. This is useful for temporarily + /// stopping playback of incoming audio without disconnecting the track. + /// This method is only available on Android platform. + /// + /// + /// + /// + /// + /// + public void MuteLocally() + { + if (_streamRenderer == null) + throw new InvalidOperationException("MuteLocally is only available for receiver side audio tracks."); + + NativeMethods.AudioTrackSinkMute(_streamRenderer.self); + } + + /// + /// Unmutes this audio track locally. + /// + /// + /// `UnmuteLocally` unmutes a previously muted audio track on the local device. + /// This method is only available on Android platform. + /// + /// + /// + /// + /// + /// + public void UnmuteLocally() + { + if (_streamRenderer == null) + throw new InvalidOperationException("UnmuteLocally is only available for receiver side audio tracks."); + + NativeMethods.AudioTrackSinkUnmute(_streamRenderer.self); + } + + /// + /// Checks if this audio track is locally muted. + /// + /// + /// `IsLocallyMuted` returns true if the track is currently muted on the local device. + /// This method is only available on Android platform. + /// + /// True if the track is locally muted, false otherwise. + /// + /// + /// + /// + /// + public bool IsLocallyMuted() + { + if (_streamRenderer == null) + throw new InvalidOperationException("IsLocallyMuted is only available for receiver side audio tracks."); + + return NativeMethods.AudioTrackSinkIsMuted(_streamRenderer.self); + } +#endif + /// /// Disposes of AudioStreamTrack. /// diff --git a/Packages/StreamVideo/Runtime/Libs/io.stream.unity.webrtc/Runtime/Scripts/WebRTC.cs b/Packages/StreamVideo/Runtime/Libs/io.stream.unity.webrtc/Runtime/Scripts/WebRTC.cs index 08db4aa1..3c81c1ad 100644 --- a/Packages/StreamVideo/Runtime/Libs/io.stream.unity.webrtc/Runtime/Scripts/WebRTC.cs +++ b/Packages/StreamVideo/Runtime/Libs/io.stream.unity.webrtc/Runtime/Scripts/WebRTC.cs @@ -1736,6 +1736,15 @@ public static extern void RegisterDebugLog(DelegateDebugLog func, [MarshalAs(Unm [DllImport(WebRTC.Lib)] public static extern void AudioTrackSinkProcessAudio( IntPtr sink, float[] data, int length, int channels, int sampleRate); +#if UNITY_ANDROID && !UNITY_EDITOR + [DllImport(WebRTC.Lib)] + public static extern void AudioTrackSinkMute(IntPtr sink); + [DllImport(WebRTC.Lib)] + public static extern void AudioTrackSinkUnmute(IntPtr sink); + [DllImport(WebRTC.Lib)] + [return: MarshalAs(UnmanagedType.U1)] + public static extern bool AudioTrackSinkIsMuted(IntPtr sink); +#endif [DllImport(WebRTC.Lib)] [return: MarshalAs(UnmanagedType.U1)] public static extern bool MediaStreamAddTrack(IntPtr stream, IntPtr track); diff --git a/Packages/StreamVideo/Samples~/VideoChat/Images/UI/Icons.meta b/Packages/StreamVideo/Samples~/VideoChat/Images/UI/Icons.meta new file mode 100644 index 00000000..8bae00fc --- /dev/null +++ b/Packages/StreamVideo/Samples~/VideoChat/Images/UI/Icons.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 134e2b75f899f0446ad0bf307cf6b05e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/StreamVideo/Samples~/VideoChat/Images/UI/Icons/mute_12226000.png b/Packages/StreamVideo/Samples~/VideoChat/Images/UI/Icons/mute_12226000.png new file mode 100644 index 00000000..39817b4e Binary files /dev/null and b/Packages/StreamVideo/Samples~/VideoChat/Images/UI/Icons/mute_12226000.png differ diff --git a/Packages/StreamVideo/Samples~/VideoChat/Images/UI/Icons/mute_12226000.png.meta b/Packages/StreamVideo/Samples~/VideoChat/Images/UI/Icons/mute_12226000.png.meta new file mode 100644 index 00000000..10892215 --- /dev/null +++ b/Packages/StreamVideo/Samples~/VideoChat/Images/UI/Icons/mute_12226000.png.meta @@ -0,0 +1,140 @@ +fileFormatVersion: 2 +guid: b2cf530f9ab71904b9b9fecd4b1e4e36 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 13 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Android + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Windows Store Apps + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/StreamVideo/Samples~/VideoChat/Prefabs/UI/Screens/ParticipantView.prefab b/Packages/StreamVideo/Samples~/VideoChat/Prefabs/UI/Screens/ParticipantView.prefab index 883dee88..9e1f2672 100644 --- a/Packages/StreamVideo/Samples~/VideoChat/Prefabs/UI/Screens/ParticipantView.prefab +++ b/Packages/StreamVideo/Samples~/VideoChat/Prefabs/UI/Screens/ParticipantView.prefab @@ -32,8 +32,9 @@ RectTransform: m_ConstrainProportionsScale: 0 m_Children: - {fileID: 7252032941131618416} + - {fileID: 7637150640667577125} + - {fileID: 3604744137779619309} m_Father: {fileID: 2006121863544806348} - m_RootOrder: 1 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 0, y: 0} @@ -62,7 +63,7 @@ MonoBehaviour: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 0, g: 0, b: 0, a: 1} - m_RaycastTarget: 1 + m_RaycastTarget: 0 m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: @@ -122,7 +123,6 @@ RectTransform: - {fileID: 2006121864280196913} - {fileID: 2006121863080428319} m_Father: {fileID: 0} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 0, y: 0} @@ -188,6 +188,11 @@ MonoBehaviour: _defaultSpeakerFrameColor: serializedVersion: 2 rgba: 0 + _forceRequestedResolution: 0 + _forceRequestedResolutionWidth: 300 + _forceRequestedResolutionHeight: 300 + _isMutedIcon: {fileID: 7852245601941594288} + _muteLocallyToggleButton: {fileID: 6073022831238027141} --- !u!1 &2006121864280196914 GameObject: m_ObjectHideFlags: 0 @@ -220,7 +225,6 @@ RectTransform: m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 2006121863544806348} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 1} m_AnchorMax: {x: 1, y: 1} @@ -344,6 +348,126 @@ MonoBehaviour: m_FlexibleWidth: -1 m_FlexibleHeight: -1 m_LayoutPriority: 1 +--- !u!1 &5510979313677697257 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3604744137779619309} + - component: {fileID: 6166759739252713308} + - component: {fileID: 6857646120311013141} + - component: {fileID: 6073022831238027141} + m_Layer: 5 + m_Name: MuteButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3604744137779619309 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5510979313677697257} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 2006121863080428319} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &6166759739252713308 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5510979313677697257} + m_CullTransparentMesh: 1 +--- !u!114 &6857646120311013141 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5510979313677697257} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!114 &6073022831238027141 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5510979313677697257} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 6857646120311013141} + m_OnClick: + m_PersistentCalls: + m_Calls: [] --- !u!1 &7624386267670761082 GameObject: m_ObjectHideFlags: 0 @@ -375,7 +499,6 @@ RectTransform: m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 2006121863080428319} - m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 1, y: 1} @@ -404,7 +527,7 @@ MonoBehaviour: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} - m_RaycastTarget: 1 + m_RaycastTarget: 0 m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: @@ -417,3 +540,99 @@ MonoBehaviour: y: 0 width: 1 height: 1 +--- !u!1 &7852245601941594288 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7637150640667577125} + - component: {fileID: 491838988929794835} + - component: {fileID: 8250186623793420590} + - component: {fileID: 7306340483036908546} + m_Layer: 5 + m_Name: IsMutedIcon + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!224 &7637150640667577125 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7852245601941594288} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 2006121863080428319} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: -10, y: 10} + m_SizeDelta: {x: 100, y: 100} + m_Pivot: {x: 1, y: 0} +--- !u!222 &491838988929794835 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7852245601941594288} + m_CullTransparentMesh: 1 +--- !u!114 &8250186623793420590 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7852245601941594288} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 21300000, guid: b2cf530f9ab71904b9b9fecd4b1e4e36, type: 3} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!114 &7306340483036908546 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7852245601941594288} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreLayout: 1 + m_MinWidth: -1 + m_MinHeight: -1 + m_PreferredWidth: -1 + m_PreferredHeight: -1 + m_FlexibleWidth: -1 + m_FlexibleHeight: -1 + m_LayoutPriority: 1 diff --git a/Packages/StreamVideo/Samples~/VideoChat/Scripts/StreamVideoManager.cs b/Packages/StreamVideo/Samples~/VideoChat/Scripts/StreamVideoManager.cs index c92ec2a1..2f402aee 100644 --- a/Packages/StreamVideo/Samples~/VideoChat/Scripts/StreamVideoManager.cs +++ b/Packages/StreamVideo/Samples~/VideoChat/Scripts/StreamVideoManager.cs @@ -1,10 +1,12 @@ using System; +using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; using StreamVideo.Core; using StreamVideo.Core.Configs; using StreamVideo.Core.Exceptions; using StreamVideo.Core.StatefulModels; +using StreamVideo.Core.StatefulModels.Tracks; using StreamVideo.Libs.Auth; using StreamVideo.Libs.Serialization; using StreamVideo.Libs.Utils; @@ -140,6 +142,33 @@ public void ToggleMusic(bool loop = true, bool forceStop = false) /// public void SetAudioREDundancyEncoding(bool value) => _clientConfig.Audio.EnableRed = value; + public void MuteLocally(IStreamVideoCallParticipant participant) + { + _isUserMutedLocally[participant.UserId] = true; + + if (participant.AudioTrack != null && participant.AudioTrack is StreamAudioTrack streamAudioTrack) + { + streamAudioTrack.MuteLocally(); + } + } + + public void UnmuteLocally(IStreamVideoCallParticipant participant) + { + if (!_isUserMutedLocally.ContainsKey(participant.UserId)) + { + return; + } + _isUserMutedLocally.Remove(participant.UserId); + + if (participant.AudioTrack != null && participant.AudioTrack is StreamAudioTrack streamAudioTrack) + { + streamAudioTrack.UnmuteLocally(); + } + } + + public bool IsParticipantMutedLocally(IStreamVideoCallParticipant participant) + => _isUserMutedLocally.ContainsKey(participant.UserId) && _isUserMutedLocally[participant.UserId]; + protected async void Start() { var credentials = new AuthCredentials(_apiKey, _userId, _userToken); @@ -271,6 +300,9 @@ private string _info private bool _wasAudioPublishEnabledOnPause; private bool _wasVideoPublishEnabledOnPause; + // We mute by user ID because Session ID will change every time the user reconnects + private readonly Dictionary _isUserMutedLocally = new Dictionary(); + private async Task ConnectToStreamAsync(AuthCredentials credentials) { var credentialsEmpty = string.IsNullOrEmpty(credentials.ApiKey) && diff --git a/Packages/StreamVideo/Samples~/VideoChat/Scripts/UI/ParticipantView.cs b/Packages/StreamVideo/Samples~/VideoChat/Scripts/UI/ParticipantView.cs index 0ea16f99..50462f31 100644 --- a/Packages/StreamVideo/Samples~/VideoChat/Scripts/UI/ParticipantView.cs +++ b/Packages/StreamVideo/Samples~/VideoChat/Scripts/UI/ParticipantView.cs @@ -12,8 +12,10 @@ public class ParticipantView : MonoBehaviour { public IStreamVideoCallParticipant Participant { get; private set; } - public void Init(IStreamVideoCallParticipant participant) + public void Init(IStreamVideoCallParticipant participant, StreamVideoManager videoManager) { + _videoManager = videoManager ?? throw new ArgumentNullException(nameof(videoManager)); + if (Participant != null) { throw new NotSupportedException("reusing participant view for new participant is not supported yet"); @@ -57,6 +59,7 @@ protected void Awake() { _videoRectTransform = _video.GetComponent(); _baseVideoRotation = _videoRectTransform.rotation; + _muteLocallyToggleButton.onClick.AddListener(OnMuteLocallyToggleClicked); } // Called by Unity Engine @@ -131,10 +134,17 @@ protected void OnDestroy() [SerializeField] private int _forceRequestedResolutionHeight = 300; + [SerializeField] + private GameObject _isMutedIcon; + + [SerializeField] + private Button _muteLocallyToggleButton; + private AudioSource _audioSource; private RectTransform _videoRectTransform; private Vector2 _lastRequestedResolution; private Quaternion _baseVideoRotation; + private StreamVideoManager _videoManager; private void OnParticipantTrackAdded(IStreamVideoCallParticipant participant, IStreamTrack track) { @@ -152,6 +162,14 @@ private void OnParticipantTrackAdded(IStreamVideoCallParticipant participant, IS _audioSource = gameObject.AddComponent(); streamAudioTrack.SetAudioSourceTarget(_audioSource); + + // Apply cached local mute state in case participant rejoined + if (_videoManager.IsParticipantMutedLocally(participant)) + { + streamAudioTrack.MuteLocally(); + UpdateMuteIcon(); + } + break; case StreamVideoTrack streamVideoTrack: @@ -162,5 +180,36 @@ private void OnParticipantTrackAdded(IStreamVideoCallParticipant participant, IS throw new ArgumentOutOfRangeException(nameof(track)); } } + + private void OnMuteLocallyToggleClicked() + { + if (Participant == null) + { + return; + } + + var isMuted = _videoManager.IsParticipantMutedLocally(Participant); + var newIsMuted = !isMuted; + + var actionLabel = newIsMuted ? "Muted" : "Unmuted"; + Debug.Log(actionLabel + " participant with user ID: " + Participant.UserId); + + if (newIsMuted) + { + _videoManager.MuteLocally(Participant); + } + else + { + _videoManager.UnmuteLocally(Participant); + } + + UpdateMuteIcon(); + } + + private void UpdateMuteIcon() + { + var isMuted = _videoManager.IsParticipantMutedLocally(Participant); + _isMutedIcon.SetActive(isMuted); + } } } \ No newline at end of file diff --git a/Packages/StreamVideo/Samples~/VideoChat/Scripts/UI/Screens/CallScreenView.cs b/Packages/StreamVideo/Samples~/VideoChat/Scripts/UI/Screens/CallScreenView.cs index 4359c1ae..23844db6 100644 --- a/Packages/StreamVideo/Samples~/VideoChat/Scripts/UI/Screens/CallScreenView.cs +++ b/Packages/StreamVideo/Samples~/VideoChat/Scripts/UI/Screens/CallScreenView.cs @@ -198,12 +198,12 @@ private void AddParticipant(IStreamVideoCallParticipant participant, bool sortPa { var parent = GetParticipantViewParent(participant); var view = Instantiate(_participantViewPrefab, parent); - view.Init(participant); + view.Init(participant, VideoManager); _participantSessionIdToView.Add(participant.SessionId, view); if (participant.IsLocalParticipant) { - // Set input camera as a video source for local participant - we won't receive OnTrack event for local participant + // Set input camera as a video source for local participant - we won't receive TrackAdded event for local participant var webCamTexture = VideoManager.Client.VideoDeviceManager.GetSelectedDeviceWebCamTexture(); view.SetLocalCameraSource(webCamTexture); //StreamTodo: this will invalidate each time WebCamTexture is internally replaced so we need a better way to expose this