Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Sources/LiveKit/Participant/LocalParticipant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ public class LocalParticipant: Participant, @unchecked Sendable {
try await publisher.remove(track: sender)
// Mark re-negotiation required...
try await room.publisherShouldNegotiate()

// Reset hasPublished when no more tracks are published, so that
// a publisher transport disconnect doesn't trigger an unnecessary reconnect.
let hasPublishedTracks = _state.trackPublications.values.contains { $0.track != nil }
if !hasPublishedTracks {
room._state.mutate { $0.hasPublished = false }
}
}

// Wait for track to stop (if required)
Expand Down
57 changes: 57 additions & 0 deletions Tests/LiveKitCoreTests/PeerConnectionSignalingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
// MARK: - PeerConnectionSignalingTests

@Suite(.serialized, .tags(.e2e))
struct PeerConnectionSignalingTests {

Check failure on line 126 in Tests/LiveKitCoreTests/PeerConnectionSignalingTests.swift

View workflow job for this annotation

GitHub Actions / Lint

Struct body should span 250 lines or less excluding comments and whitespace: currently spans 272 lines (type_body_length)
// MARK: - Helpers

private func roomTestingOptions(
Expand Down Expand Up @@ -372,6 +372,63 @@
}
}

@Test(.bug("https://github.com/livekit/client-sdk-swift/issues/958"),
arguments: SignalingMode.allCases)
func republishAfterUnpublishAll(mode: SignalingMode) async throws {
try await TestEnvironment.withRooms([
roomTestingOptions(mode: mode, canPublish: true),
roomTestingOptions(mode: mode, canSubscribe: true),
]) { rooms in
let room1 = rooms[0]
let room2 = rooms[1]

let publisherIdentity = try #require(room1.localParticipant.identity)
let remoteParticipant = try #require(room2.remoteParticipants[publisherIdentity])

// Phase 1: Publish audio + simulcast video (720x1280 triggers 3 layers)
let audioTrack1 = TestAudioTrack(name: "audio-1")
let videoTrack1 = LocalVideoTrack.createBufferTrack(name: "video-1")
if let capturer = videoTrack1.capturer as? BufferCapturer {
var pb: CVPixelBuffer?
CVPixelBufferCreate(kCFAllocatorDefault, 720, 1280, kCVPixelFormatType_32BGRA, nil, &pb)
if let pb { capturer.capture(pb) }
}

try await room1.localParticipant.publish(audioTrack: audioTrack1)
try await room1.localParticipant.publish(videoTrack: videoTrack1)

try await waitForPublish(on: remoteParticipant) {
$0.trackPublications.count >= 2
}

// Phase 2: Unpublish all
await room1.localParticipant.unpublishAll()

try await waitForPublish(on: remoteParticipant) {
$0.trackPublications.isEmpty
}

try await Task.sleep(nanoseconds: 2_000_000_000)

// Phase 3: Republish new tracks
let audioTrack2 = TestAudioTrack(name: "audio-2")
let videoTrack2 = LocalVideoTrack.createBufferTrack(name: "video-2")
if let capturer = videoTrack2.capturer as? BufferCapturer {
var pb: CVPixelBuffer?
CVPixelBufferCreate(kCFAllocatorDefault, 720, 1280, kCVPixelFormatType_32BGRA, nil, &pb)
if let pb { capturer.capture(pb) }
}

try await room1.localParticipant.publish(audioTrack: audioTrack2)
try await room1.localParticipant.publish(videoTrack: videoTrack2)

try await waitForPublish(on: remoteParticipant) {
$0.trackPublications.count >= 2
}
#expect(remoteParticipant.trackPublications.count >= 2, "Remote should see republished tracks")
}
}

@Test(arguments: SignalingMode.allCases)
func doubleReconnect(mode: SignalingMode) async throws {
let reconnectWatcher = ReconnectWatcher()
Expand Down
Loading