Skip to content

Commit 1637958

Browse files
authored
[Feature]Screenshare audio (#1020)
1 parent de5a14f commit 1637958

File tree

61 files changed

+2717
-176
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+2717
-176
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
44

55
# Upcoming
66

7-
### 🔄 Changed
7+
### ✅ Added
8+
- Support for capture inApp audio during ScreenSharing sessions. [#1020](https://github.com/GetStream/stream-video-swift/pull/1020)
89

910
# [1.38.2](https://github.com/GetStream/stream-video-swift/releases/tag/1.38.2)
1011
_December 22, 2025_
2.01 MB
Binary file not shown.
2.06 MB
Binary file not shown.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//
2+
// Copyright © 2025 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import AVFoundation
6+
import Foundation
7+
import StreamVideo
8+
9+
final class AudioTrackPlayer: NSObject, AVAudioPlayerDelegate {
10+
enum Track: String, Equatable, CaseIterable {
11+
case track1 = "track_1"
12+
case track2 = "track_2"
13+
14+
var fileExtension: String {
15+
switch self {
16+
case .track1:
17+
return ".mp3"
18+
case .track2:
19+
return ".mp3"
20+
}
21+
}
22+
}
23+
24+
@Published private(set) var isPlaying: Bool = false
25+
@Published private(set) var track: Track?
26+
27+
private var audioPlayer: AVAudioPlayer?
28+
private let processingQueue = OperationQueue(maxConcurrentOperationCount: 1)
29+
30+
func play(_ track: Track) {
31+
processingQueue.addTaskOperation { @MainActor [weak self] in
32+
guard
33+
let self,
34+
self.track != track,
35+
let url = Bundle.main.url(forResource: track.rawValue, withExtension: track.fileExtension),
36+
let audioPlayer = try? AVAudioPlayer(contentsOf: url)
37+
else {
38+
return
39+
}
40+
41+
self.audioPlayer = audioPlayer
42+
audioPlayer.play()
43+
audioPlayer.numberOfLoops = 1000
44+
self.track = track
45+
self.isPlaying = true
46+
}
47+
}
48+
49+
func stop() {
50+
processingQueue.addTaskOperation { @MainActor [weak self] in
51+
guard
52+
let self
53+
else {
54+
return
55+
}
56+
57+
audioPlayer?.stop()
58+
audioPlayer = nil
59+
isPlaying = false
60+
track = nil
61+
}
62+
}
63+
64+
// MARK: - AVAudioPlayerDelegate
65+
66+
func audioPlayerDidFinishPlaying(
67+
_ player: AVAudioPlayer,
68+
successfully flag: Bool
69+
) {
70+
stop()
71+
}
72+
}
73+
74+
extension AudioTrackPlayer: InjectionKey {
75+
static var currentValue: AudioTrackPlayer = .init()
76+
}
77+
78+
extension InjectedValues {
79+
var audioPlayer: AudioTrackPlayer {
80+
get { Self[AudioTrackPlayer.self] }
81+
set { _ = newValue }
82+
}
83+
}

DemoApp/Sources/Controls/DemoMoreControls/DemoBroadcastMoreControlsListButtonView.swift

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,29 @@ struct DemoBroadcastMoreControlsListButtonView: View {
4040

4141
@ViewBuilder
4242
private var inAppScreenshareButtonView: some View {
43-
DemoMoreControlListButtonView(
44-
action: {
45-
viewModel.startScreensharing(type: .inApp)
43+
Menu {
44+
Button {
45+
viewModel.startScreensharing(type: .inApp, includeAudio: false)
4646
selection = .inApp
47-
},
48-
label: "Screenshare"
49-
) {
50-
Image(systemName: "record.circle")
51-
.foregroundColor(appearance.colors.text)
47+
} label: {
48+
Text("Without audio")
49+
}
50+
51+
Button {
52+
viewModel.startScreensharing(type: .inApp, includeAudio: true)
53+
selection = .inApp
54+
} label: {
55+
Text("With audio")
56+
}
57+
58+
} label: {
59+
DemoMoreControlListButtonView(
60+
action: {},
61+
label: "Screenshare"
62+
) {
63+
Image(systemName: "record.circle")
64+
.foregroundColor(appearance.colors.text)
65+
}
5266
}
5367
}
5468

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//
2+
// Copyright © 2025 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import Foundation
6+
import StreamVideo
7+
import SwiftUI
8+
9+
struct DemoAudioTrackButtonView: View {
10+
@Injected(\.audioPlayer) var audioPlayer: AudioTrackPlayer
11+
12+
@State private var isPlaying: Bool = AudioTrackPlayer.currentValue.isPlaying
13+
@State private var track: AudioTrackPlayer.Track? = AudioTrackPlayer.currentValue.track
14+
15+
var body: some View {
16+
Menu {
17+
Button {
18+
audioPlayer.stop()
19+
} label: {
20+
Label {
21+
Text("None")
22+
} icon: {
23+
if track == nil {
24+
Image(systemName: "checkmark")
25+
}
26+
}
27+
}
28+
29+
Divider()
30+
31+
ForEach(AudioTrackPlayer.Track.allCases, id: \.self) { track in
32+
Button {
33+
if self.track == track {
34+
audioPlayer.stop()
35+
} else {
36+
audioPlayer.play(track)
37+
}
38+
} label: {
39+
Label {
40+
Text(track.rawValue)
41+
} icon: {
42+
if self.track == track {
43+
Image(systemName: "checkmark")
44+
}
45+
}
46+
}
47+
}
48+
} label: {
49+
DemoMoreControlListButtonView(
50+
action: {},
51+
label: "In-App audio"
52+
) {
53+
Image(
54+
systemName: isPlaying ? "pause.circle" : "play.circle"
55+
)
56+
}
57+
}
58+
.onReceive(audioPlayer.$isPlaying.receive(on: DispatchQueue.main)) { isPlaying = $0 }
59+
.onReceive(audioPlayer.$track.receive(on: DispatchQueue.main)) { track = $0 }
60+
}
61+
}

DemoApp/Sources/ViewModifiers/MoreControls/DemoMoreControlsViewModifier.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ struct DemoMoreControlsViewModifier: ViewModifier {
7777
VStack {
7878
Divider()
7979

80+
DemoAudioTrackButtonView()
81+
8082
DemoMoreLogsAndGleapButtonView()
8183

8284
DemoBroadcastMoreControlsListButtonView(

DocumentationTests/DocumentationTests/DocumentationTests/06-advanced/04-screensharing.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ private func content() {
1717
}
1818
}
1919

20+
asyncContainer {
21+
Task {
22+
let call = streamVideo.call(callType: "default", callId: "123")
23+
try await call.join()
24+
try await call.startScreensharing(type: .inApp, includeAudio: true)
25+
}
26+
}
27+
2028
asyncContainer {
2129
Task {
2230
try await call.stopScreensharing()

Sources/StreamVideo/Call.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -546,9 +546,18 @@ public class Call: @unchecked Sendable, WSEventsSubscriber {
546546
}
547547

548548
/// Starts screensharing from the device.
549-
/// - Parameter type: The screensharing type (in-app or broadcasting).
550-
public func startScreensharing(type: ScreensharingType) async throws {
551-
try await callController.startScreensharing(type: type)
549+
/// - Parameters:
550+
/// - type: The screensharing type (in-app or broadcasting).
551+
/// - includeAudio: Whether to capture app audio during screensharing.
552+
/// Only valid for `.inApp`; ignored otherwise.
553+
public func startScreensharing(
554+
type: ScreensharingType,
555+
includeAudio: Bool = true
556+
) async throws {
557+
try await callController.startScreensharing(
558+
type: type,
559+
includeAudio: includeAudio
560+
)
552561
}
553562

554563
/// Stops screensharing from the current device.

Sources/StreamVideo/Controllers/CallController.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,19 @@ class CallController: @unchecked Sendable {
211211
}
212212
}
213213

214-
func startScreensharing(type: ScreensharingType) async throws {
215-
try await webRTCCoordinator.startScreensharing(type: type)
214+
/// Starts screensharing for the current call.
215+
/// - Parameters:
216+
/// - type: The screensharing type (in-app or broadcasting).
217+
/// - includeAudio: Whether to capture app audio during screensharing.
218+
/// Only valid for `.inApp`; ignored otherwise.
219+
func startScreensharing(
220+
type: ScreensharingType,
221+
includeAudio: Bool
222+
) async throws {
223+
try await webRTCCoordinator.startScreensharing(
224+
type: type,
225+
includeAudio: includeAudio
226+
)
216227
}
217228

218229
func stopScreensharing() async throws {

0 commit comments

Comments
 (0)