diff --git a/.changeset/datatrack-republish-rejected.md b/.changeset/datatrack-republish-rejected.md new file mode 100644 index 0000000000..fcb274afab --- /dev/null +++ b/.changeset/datatrack-republish-rejected.md @@ -0,0 +1,5 @@ +--- +"livekit-client": patch +--- + +Mark data track unpublished after republish fail diff --git a/src/room/data-track/outgoing/OutgoingDataTrackManager.test.ts b/src/room/data-track/outgoing/OutgoingDataTrackManager.test.ts index 9d22221130..c8c76acdb5 100644 --- a/src/room/data-track/outgoing/OutgoingDataTrackManager.test.ts +++ b/src/room/data-track/outgoing/OutgoingDataTrackManager.test.ts @@ -14,7 +14,7 @@ import OutgoingDataTrackManager, { type DataTrackOutgoingManagerCallbacks, Descriptor, } from './OutgoingDataTrackManager'; -import { DataTrackPublishError } from './errors'; +import { DataTrackPublishError, DataTrackPushFrameErrorReason } from './errors'; /** Fake encryption provider for testing e2ee data track features. */ export class PrefixingEncryptionProvider implements BaseE2EEManager { @@ -521,6 +521,46 @@ describe('DataTrackOutgoingManager', () => { await managerEvents.waitFor('packetAvailable'); }); + it('should unpublish a track when republish is rejected', async () => { + const manager = new OutgoingDataTrackManager(); + const managerEvents = subscribeToEvents(manager, [ + 'sfuPublishRequest', + ]); + + const localDataTrack = new LocalDataTrack({ name: 'track' }, manager); + const publishRequestPromise = localDataTrack.publish(); + + const sfuPublishEvent = await managerEvents.waitFor('sfuPublishRequest'); + const handle = sfuPublishEvent.handle; + + manager.receivedSfuPublishResponse(handle, { + type: 'ok', + data: { + sid: 'bogus-sid', + pubHandle: handle, + name: 'track', + usesE2ee: false, + }, + }); + await publishRequestPromise; + + // Simulate reconnect + manager.sfuWillRepublishTracks(); + await managerEvents.waitFor('sfuPublishRequest'); + + // SFU rejects the republish + manager.receivedSfuPublishResponse(handle, { + type: 'error', + error: DataTrackPublishError.notAllowed(), + }); + + // Track is unpublished + expect(localDataTrack.isPublished()).toStrictEqual(false); + await expect(() => + localDataTrack.tryPush({ payload: new Uint8Array([0xff]) }), + ).rejects.toMatchObject({ reason: DataTrackPushFrameErrorReason.TrackUnpublished }); + }); + it('should query currently active descriptors', async () => { // Create a manager prefilled with a descriptor const manager = OutgoingDataTrackManager.withDescriptors( diff --git a/src/room/data-track/outgoing/OutgoingDataTrackManager.ts b/src/room/data-track/outgoing/OutgoingDataTrackManager.ts index 70fb4115c8..a07f658279 100644 --- a/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +++ b/src/room/data-track/outgoing/OutgoingDataTrackManager.ts @@ -343,7 +343,8 @@ export default class OutgoingDataTrackManager extends (EventEmitter as new () => return; } if (result.type === 'error') { - log.warn(`Republish failed for track ${handle}`); + log.warn(`Republish failed for track ${handle}: ${result.error}`); + descriptor.publishState = 'unpublished'; return; } @@ -380,6 +381,7 @@ export default class OutgoingDataTrackManager extends (EventEmitter as new () => * * This must be sent after a full reconnect in order for existing publications * to be recognized by the SFU. Each republished track will be assigned a new SID. + * Tracks the SFU rejects are unpublished. * @internal */ sfuWillRepublishTracks() {