Skip to content

Commit f9959c6

Browse files
committed
update: implement SABR recovery mechanism for stream stalls
1 parent 2769753 commit f9959c6

File tree

2 files changed

+63
-0
lines changed

2 files changed

+63
-0
lines changed

src/sources/youtube/YouTube.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -930,6 +930,8 @@ export default class YouTubeSource {
930930
})
931931

932932
const stream = new PassThrough()
933+
let isRecovering = false
934+
let lastRecoverAt = 0
933935

934936
sabr.on('data', (chunk) => {
935937
if (!stream.write(chunk)) {
@@ -940,6 +942,48 @@ export default class YouTubeSource {
940942

941943
sabr.on('end', () => stream.end())
942944
sabr.on('finishBuffering', () => stream.emit('finishBuffering'))
945+
sabr.on('stall', async () => {
946+
if (isRecovering || stream.destroyed) return
947+
948+
const now = Date.now()
949+
if (now - lastRecoverAt < 2000) return
950+
lastRecoverAt = now
951+
952+
isRecovering = true
953+
try {
954+
logger(
955+
'warn',
956+
'YouTube',
957+
`SABR stall detected for ${decodedTrack.title}. Refreshing session...`
958+
)
959+
const newUrlData = await this.getTrackUrl(decodedTrack, null, true)
960+
if (!newUrlData || newUrlData.protocol !== 'sabr') {
961+
throw new Error('No SABR session available for recovery')
962+
}
963+
964+
const ad = newUrlData.additionalData || {}
965+
sabr.clearBuffers()
966+
sabr.updateSession({
967+
serverAbrStreamingUrl: ad.serverAbrStreamingUrl || newUrlData.url,
968+
videoPlaybackUstreamerConfig: ad.videoPlaybackUstreamerConfig,
969+
poToken: ad.poToken,
970+
visitorData: ad.visitorData,
971+
clientInfo: ad.clientInfo,
972+
formats: ad.formats,
973+
userAgent: ad.userAgent,
974+
playbackCookie: ad.playbackCookie
975+
})
976+
} catch (err) {
977+
logger(
978+
'warn',
979+
'YouTube',
980+
`SABR recovery failed: ${err.message}`
981+
)
982+
if (!stream.destroyed) stream.destroy(err)
983+
} finally {
984+
isRecovering = false
985+
}
986+
})
943987
sabr.on('error', async (err) => {
944988
logger('error', 'YouTube', `SABR stream error: ${err.message}`)
945989

src/sources/youtube/sabr/sabr.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ export class SabrStream extends PassThrough {
282282
this.startTime = config.startTime || 0;
283283
this.positionCallback = config.positionCallback;
284284
this.userAgent = config.userAgent || USER_AGENT;
285+
this.recoveryPending = false;
285286

286287

287288

@@ -354,6 +355,10 @@ export class SabrStream extends PassThrough {
354355
try {
355356
if (this.lastVirtualAdvanceAt === 0) this.lastVirtualAdvanceAt = Date.now();
356357
while (!this._aborted && !this.destroyed && !this.streamFinished) {
358+
if (this.recoveryPending) {
359+
await wait(500, signal);
360+
continue;
361+
}
357362
if (this.requestNumber === 0) {
358363
try {
359364
const tokenData = await poTokenManager.generate(this.videoId, this.visitorData);
@@ -485,6 +490,18 @@ export class SabrStream extends PassThrough {
485490
logger('error', 'SABR', `Failed to decode PO token (session update): ${e.message}`);
486491
}
487492
}
493+
if (config.visitorData) {
494+
this.visitorData = config.visitorData;
495+
}
496+
if (config.clientInfo) {
497+
this.clientInfo = config.clientInfo;
498+
}
499+
if (config.formats) {
500+
this.formatIds = config.formats;
501+
}
502+
if (config.userAgent) {
503+
this.userAgent = config.userAgent;
504+
}
488505
if (config.playbackCookie) {
489506
if (!this.nextRequestPolicy) this.nextRequestPolicy = {};
490507
this.nextRequestPolicy.playbackCookie = config.playbackCookie;
@@ -494,6 +511,7 @@ export class SabrStream extends PassThrough {
494511
this.requestNumber = 0;
495512
this.noMediaStreak = 0;
496513
this.pendingRangesHeaders.clear();
514+
this.recoveryPending = false;
497515

498516
logger('info', 'SABR', `Session updated. Continuing with RN=${this.requestNumber}, URL=${this.serverAbrStreamingUrl.slice(0, 50)}...`);
499517
}
@@ -616,6 +634,7 @@ export class SabrStream extends PassThrough {
616634
if (status.status === 2) {
617635
logger('warn', 'SABR', `Stream Protection Status: ${status.status} (Limited Playback). Triggering token refresh...`);
618636
poTokenManager.reset();
637+
this.recoveryPending = true;
619638
this.emit('stall');
620639
return;
621640
}

0 commit comments

Comments
 (0)