From 0e55d5cbe31d7c90a067cbe0806be1849aa24c50 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 12 May 2026 17:43:26 +0800 Subject: [PATCH 1/5] slider --- Multiplatform/Views/ParticipantView.swift | 87 +++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/Multiplatform/Views/ParticipantView.swift b/Multiplatform/Views/ParticipantView.swift index ba6faab..2bf2e23 100644 --- a/Multiplatform/Views/ParticipantView.swift +++ b/Multiplatform/Views/ParticipantView.swift @@ -28,6 +28,12 @@ struct ParticipantView: View { @State private var isRendering: Bool = false + private var remoteAudioTracks: [RemoteAudioTrack] { + participant.audioTracks + .compactMap { $0.track as? RemoteAudioTrack } + .sorted { $0.id < $1.id } + } + func bgView(systemSymbol: SFSymbol, geometry: GeometryProxy) -> some View { Image(systemSymbol: systemSymbol) .resizable() @@ -119,6 +125,13 @@ struct ParticipantView: View { .padding() } + ForEach(remoteAudioTracks) { remoteAudioTrack in + RemoteAudioVolumeControl(track: remoteAudioTrack, + showsPercentage: geometry.size.width > 180) + .padding(.horizontal, 8) + .padding(.bottom, 6) + } + // Bottom user info bar HStack { if let identity = participant.identity { @@ -255,6 +268,80 @@ struct ParticipantView: View { } } +struct RemoteAudioVolumeControl: View { + let track: RemoteAudioTrack + let showsPercentage: Bool + + private static let defaultVolume = 1.0 + private static let maxVolume = 2.0 + private static let snapVolume = 1.0 + private static let snapThreshold = 0.04 + private static let sdkVolumeScale = 10.0 + + @State private var volume: Double + + init(track: RemoteAudioTrack, showsPercentage: Bool) { + self.track = track + self.showsPercentage = showsPercentage + _volume = State(initialValue: Self.defaultVolume) + } + + var body: some View { + HStack(spacing: 8) { + Image(systemSymbol: volume == 0 ? .speakerSlashFill : .speakerWave2Fill) + .foregroundColor(.white) + .frame(width: 18) + + Slider(value: volumeBinding, in: 0.0 ... Self.maxVolume) + .tint(Color.orange) + + if showsPercentage { + Text("\(Int((Self.clamped(volume) * 100).rounded()))%") + .font(.system(size: 10, weight: .semibold, design: .monospaced)) + .foregroundColor(.white) + .frame(width: 42, alignment: .trailing) + } + } + .padding(.horizontal, 8) + .padding(.vertical, 6) + .frame(maxWidth: 280) + .background(Color.black.opacity(0.5)) + .cornerRadius(8) + .onAppear { + let currentVolume = Self.displayVolume(from: track.volume) + setVolume(currentVolume > Self.maxVolume ? Self.defaultVolume : currentVolume) + } + } + + private var volumeBinding: Binding { + Binding( + get: { volume }, + set: { newValue in + let snappedVolume = Self.snapped(Self.clamped(newValue)) + setVolume(snappedVolume) + } + ) + } + + private func setVolume(_ newValue: Double) { + let clampedVolume = Self.clamped(newValue) + volume = clampedVolume + track.volume = clampedVolume / Self.sdkVolumeScale + } + + private static func clamped(_ volume: Double) -> Double { + min(max(volume, 0.0), maxVolume) + } + + private static func snapped(_ volume: Double) -> Double { + abs(volume - snapVolume) < snapThreshold ? defaultVolume : volume + } + + private static func displayVolume(from sdkVolume: Double) -> Double { + sdkVolume * sdkVolumeScale + } +} + struct StatsView: View { private let track: Track @ObservedObject private var observer: TrackDelegateObserver From bd5699f004b0d7a24f685d0f9f9be240b89e9cc8 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Tue, 12 May 2026 17:49:14 +0800 Subject: [PATCH 2/5] Update ParticipantView.swift --- Multiplatform/Views/ParticipantView.swift | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/Multiplatform/Views/ParticipantView.swift b/Multiplatform/Views/ParticipantView.swift index 2bf2e23..aa17d90 100644 --- a/Multiplatform/Views/ParticipantView.swift +++ b/Multiplatform/Views/ParticipantView.swift @@ -276,14 +276,13 @@ struct RemoteAudioVolumeControl: View { private static let maxVolume = 2.0 private static let snapVolume = 1.0 private static let snapThreshold = 0.04 - private static let sdkVolumeScale = 10.0 @State private var volume: Double init(track: RemoteAudioTrack, showsPercentage: Bool) { self.track = track self.showsPercentage = showsPercentage - _volume = State(initialValue: Self.defaultVolume) + _volume = State(initialValue: Self.clamped(track.volume)) } var body: some View { @@ -308,8 +307,7 @@ struct RemoteAudioVolumeControl: View { .background(Color.black.opacity(0.5)) .cornerRadius(8) .onAppear { - let currentVolume = Self.displayVolume(from: track.volume) - setVolume(currentVolume > Self.maxVolume ? Self.defaultVolume : currentVolume) + setVolume(track.volume) } } @@ -326,7 +324,7 @@ struct RemoteAudioVolumeControl: View { private func setVolume(_ newValue: Double) { let clampedVolume = Self.clamped(newValue) volume = clampedVolume - track.volume = clampedVolume / Self.sdkVolumeScale + track.volume = clampedVolume } private static func clamped(_ volume: Double) -> Double { @@ -336,10 +334,6 @@ struct RemoteAudioVolumeControl: View { private static func snapped(_ volume: Double) -> Double { abs(volume - snapVolume) < snapThreshold ? defaultVolume : volume } - - private static func displayVolume(from sdkVolume: Double) -> Double { - sdkVolume * sdkVolumeScale - } } struct StatsView: View { From 6db6060cccb64eaa91cecb9dfc8ece8c17a5f24a Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Wed, 13 May 2026 18:25:38 +0800 Subject: [PATCH 3/5] design --- Multiplatform/Views/ParticipantView.swift | 95 ++++++++++++++++------- 1 file changed, 69 insertions(+), 26 deletions(-) diff --git a/Multiplatform/Views/ParticipantView.swift b/Multiplatform/Views/ParticipantView.swift index aa17d90..3063ec4 100644 --- a/Multiplatform/Views/ParticipantView.swift +++ b/Multiplatform/Views/ParticipantView.swift @@ -125,13 +125,6 @@ struct ParticipantView: View { .padding() } - ForEach(remoteAudioTracks) { remoteAudioTrack in - RemoteAudioVolumeControl(track: remoteAudioTrack, - showsPercentage: geometry.size.width > 180) - .padding(.horizontal, 8) - .padding(.bottom, 6) - } - // Bottom user info bar HStack { if let identity = participant.identity { @@ -228,6 +221,12 @@ struct ParticipantView: View { .foregroundColor(Color.white) } + ForEach(remoteAudioTracks) { remoteAudioTrack in + RemoteAudioVolumeControl(track: remoteAudioTrack, + showsPercentage: geometry.size.width > 180) + .fixedSize() + } + if participant.connectionQuality == .excellent { Image(systemSymbol: .wifi) .foregroundColor(.green) @@ -278,6 +277,7 @@ struct RemoteAudioVolumeControl: View { private static let snapThreshold = 0.04 @State private var volume: Double + @State private var isSliderPresented = false init(track: RemoteAudioTrack, showsPercentage: Bool) { self.track = track @@ -286,31 +286,59 @@ struct RemoteAudioVolumeControl: View { } var body: some View { - HStack(spacing: 8) { - Image(systemSymbol: volume == 0 ? .speakerSlashFill : .speakerWave2Fill) - .foregroundColor(.white) - .frame(width: 18) - - Slider(value: volumeBinding, in: 0.0 ... Self.maxVolume) - .tint(Color.orange) - - if showsPercentage { - Text("\(Int((Self.clamped(volume) * 100).rounded()))%") - .font(.system(size: 10, weight: .semibold, design: .monospaced)) - .foregroundColor(.white) - .frame(width: 42, alignment: .trailing) - } + Button { + isSliderPresented.toggle() + } label: { + Image(systemSymbol: volumeSymbol) + .foregroundColor(isSliderPresented ? Color.orange : .white) + } + .buttonStyle(.plain) + .accessibilityLabel("Remote audio volume") + .accessibilityValue(volumePercentageText) + .popover(isPresented: $isSliderPresented, arrowEdge: .bottom) { + volumePopover + .remoteAudioVolumePopoverStyle() } - .padding(.horizontal, 8) - .padding(.vertical, 6) - .frame(maxWidth: 280) - .background(Color.black.opacity(0.5)) - .cornerRadius(8) .onAppear { setVolume(track.volume) } } + private var volumePopover: some View { + VStack(alignment: .leading, spacing: 10) { + HStack(spacing: 8) { + Image(systemSymbol: volumeSymbol) + .foregroundColor(Color.orange) + + Text("Volume") + .font(.headline) + + Spacer() + + Text(volumePercentageText) + .font(.system(size: 12, weight: .semibold, design: .monospaced)) + .foregroundColor(.secondary) + } + + Slider(value: volumeBinding, in: 0.0 ... Self.maxVolume) + .tint(Color.orange) + } + .padding() + .frame(width: sliderWidth) + } + + private var sliderWidth: CGFloat { + showsPercentage ? 240 : 196 + } + + private var volumeSymbol: SFSymbol { + volume == 0 ? .speakerSlashFill : .speakerWave2Fill + } + + private var volumePercentageText: String { + "\(Int((Self.clamped(volume) * 100).rounded()))%" + } + private var volumeBinding: Binding { Binding( get: { volume }, @@ -336,6 +364,21 @@ struct RemoteAudioVolumeControl: View { } } +private extension View { + @ViewBuilder + func remoteAudioVolumePopoverStyle() -> some View { + #if os(iOS) + if #available(iOS 16.4, *) { + presentationCompactAdaptation(.popover) + } else { + self + } + #else + self + #endif + } +} + struct StatsView: View { private let track: Track @ObservedObject private var observer: TrackDelegateObserver From a569ce1798592aaef1a89302b19c8f1a1d859e38 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Wed, 13 May 2026 18:26:16 +0800 Subject: [PATCH 4/5] Update Package.resolved --- .../xcshareddata/swiftpm/Package.resolved | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved index c0bcc33..4e6169b 100644 --- a/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/LiveKitExample-dev.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "fc0950f1832ca45dc3170b94ec97b2189c6b9ddb96acae64bf8148847c58d6f9", + "originHash" : "aea8f28b98f95e948f09d2d3f56fa02d451e94fb164d44b0b7fa44e15cf7fae5", "pins" : [ { "identity" : "keychainaccess", @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/livekit/livekit-uniffi-xcframework.git", "state" : { - "revision" : "61229f4032131311b997ddb1bc1cb8f5afbe30c8", - "version" : "0.0.5" + "revision" : "7c161254ce7cd55debc48023f69a917076b12a26", + "version" : "0.0.6" } }, { @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/livekit/webrtc-xcframework.git", "state" : { - "revision" : "f6017e189972b86f90e0ec406b1629828bc75748", - "version" : "144.7559.3" + "revision" : "6a5f458cf55a598e4590cb30b739ab545fdbfcb1", + "version" : "144.7559.6" } } ], From 219f0604c3d42f229cd33395849f56c31ebfd41c Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Wed, 13 May 2026 20:13:31 +0800 Subject: [PATCH 5/5] fix --- Multiplatform/Views/ParticipantView.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Multiplatform/Views/ParticipantView.swift b/Multiplatform/Views/ParticipantView.swift index 3063ec4..c320e32 100644 --- a/Multiplatform/Views/ParticipantView.swift +++ b/Multiplatform/Views/ParticipantView.swift @@ -299,9 +299,6 @@ struct RemoteAudioVolumeControl: View { volumePopover .remoteAudioVolumePopoverStyle() } - .onAppear { - setVolume(track.volume) - } } private var volumePopover: some View {