Skip to content

Commit 1e4dbbd

Browse files
improve: fully refactor the sabr implementation
1 parent 2087e0a commit 1e4dbbd

File tree

8 files changed

+1098
-284
lines changed

8 files changed

+1098
-284
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"@performanc/voice": "github:PerformanC/voice",
1818
"@toddynnn/symphonia-decoder": "1.0.6",
1919
"@toddynnn/voice-opus": "^1.0.1",
20+
"jsdom": "^27.4.0",
2021
"mp4box": "^2.3.0",
2122
"myzod": "^1.12.1"
2223
},
@@ -30,4 +31,4 @@
3031
"optionalDependencies": {
3132
"prom-client": "^15.1.3"
3233
}
33-
}
34+
}

src/playback/player.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,7 @@ export class Player {
590590
audioTrackId: this.track.audioTrackId
591591
}
592592
const urlData = await this.nodelink.sources.getTrackUrl(trackInfo)
593+
if (!this.track) return false
593594
this.streamInfo = { ...urlData, trackInfo: this.track.info }
594595
logger('debug', 'Player', `Got track URL for guild ${this.guildId}`, {
595596
urlData
@@ -796,6 +797,7 @@ export class Player {
796797
audioTrackId: this.track.audioTrackId
797798
}
798799
const urlData = await this.nodelink.sources.getTrackUrl(trackInfo)
800+
if (!this.track) return false
799801
this.streamInfo = { ...urlData, trackInfo: this.track.info }
800802
logger(
801803
'debug',
@@ -926,6 +928,7 @@ export class Player {
926928
audioTrackId: this.track.audioTrackId
927929
}
928930
const urlData = await this.nodelink.sources.getTrackUrl(trackInfo)
931+
if (!this.track) return false
929932
this.streamInfo = { ...urlData, trackInfo: this.track.info }
930933

931934
if (urlData.exception) {

src/sources/youtube/YouTube.js

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ async function _manageYoutubeHlsStream(
232232

233233
if (res.error || res.statusCode !== 200) {
234234
if (res.stream) res.stream.destroy()
235-
235+
236236
let retryCount = 0
237237
let success = false
238238
while (retryCount < 3 && !cancelSignal.aborted) {
@@ -978,7 +978,8 @@ export default class YouTubeSource {
978978
}
979979

980980
async getTrackUrl(decodedTrack, itag) {
981-
const clientList = this.config.clients.playback
981+
let clientList = [...this.config.clients.playback]
982+
clientList = ['Web', ...clientList.filter(c => c !== 'Web') || 'Android']
982983
const clientErrors = []
983984

984985
for (const clientName of clientList) {
@@ -1012,6 +1013,16 @@ export default class YouTubeSource {
10121013
}
10131014

10141015
if (urlData.protocol === 'sabr') {
1016+
const bestAudio = urlData.formats
1017+
?.filter((f) => f.mimeType?.includes('audio'))
1018+
.sort((a, b) => (b.bitrate || 0) - (a.bitrate || 0))[0]
1019+
1020+
if (bestAudio) {
1021+
urlData.format = bestAudio.mimeType?.includes('webm')
1022+
? 'webm/opus'
1023+
: 'm4a'
1024+
}
1025+
10151026
return urlData
10161027
}
10171028

@@ -1176,15 +1187,45 @@ export default class YouTubeSource {
11761187

11771188
const stream = new PassThrough()
11781189

1179-
sabr.on('data', (chunk) => stream.write(chunk))
1190+
sabr.on('data', (chunk) => {
1191+
if (!stream.write(chunk)) {
1192+
sabr.pause()
1193+
}
1194+
})
1195+
stream.on('drain', () => sabr.resume())
1196+
11801197
sabr.on('end', () => stream.end())
1181-
sabr.on('error', (err) => {
1198+
sabr.on('error', async (err) => {
11821199
logger('error', 'YouTube', `SABR stream error: ${err.message}`)
1183-
stream.destroy(err)
1200+
1201+
if ((err.message.includes('sabr.malformed_config') || err.message.includes('sabr.media_serving_enforcement_id_error')) && !isRecovering) {
1202+
logger('info', 'YouTube', `Known recoverable error detected (${err.message}), triggering stall recovery...`)
1203+
sabr.emit('stall')
1204+
return
1205+
}
1206+
1207+
if (!stream.destroyed) stream.destroy(err)
1208+
})
1209+
1210+
const originalDestroy = stream.destroy.bind(stream)
1211+
let isDestroying = false
1212+
stream.destroy = (err) => {
1213+
if (isDestroying) return
1214+
isDestroying = true
1215+
sabr.destroy(err)
1216+
this.activeStreams.delete(streamKey)
1217+
originalDestroy(err)
1218+
}
1219+
1220+
stream.once('close', () => {
1221+
if (isDestroying) return
1222+
isDestroying = true
1223+
sabr.destroy()
1224+
this.activeStreams.delete(streamKey)
11841225
})
11851226

11861227
const bestAudio = additionalData.formats.filter(f => f.mimeType?.includes('audio')).sort((a, b) => (b.bitrate || 0) - (a.bitrate || 0))[0]
1187-
1228+
11881229
sabr.start(bestAudio.itag)
11891230

11901231
const type = bestAudio.mimeType?.includes('webm') ? 'webm/opus' : 'm4a'

src/sources/youtube/clients/Android.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,46 @@ export default class Android extends BaseClient {
314314
return { exception: { message, severity: 'common', cause: 'Upstream' } }
315315
}
316316

317+
const streamingData = playerResponse.streamingData || playerResponse.streaming_data
318+
const serverAbrUrl = streamingData?.serverAbrStreamingUrl || streamingData?.server_abr_streaming_url
319+
const ustreamerConfig = playerResponse.playerConfig?.mediaCommonConfig?.mediaUstreamerRequestConfig?.videoPlaybackUstreamerConfig
320+
321+
if (serverAbrUrl) {
322+
logger('debug', 'YouTube-Android', `SABR URL found for ${decodedTrack.identifier}. Using SABR protocol.`)
323+
324+
const formats = [...(streamingData.formats || []), ...(streamingData.adaptiveFormats || streamingData.adaptive_formats || [])].map(f => ({
325+
itag: f.itag,
326+
lastModified: f.lastModified || f.last_modified_ms,
327+
xtags: f.xtags,
328+
width: f.width,
329+
height: f.height,
330+
mimeType: f.mimeType || f.mime_type,
331+
audioQuality: f.audioQuality || f.audio_quality,
332+
bitrate: f.bitrate,
333+
averageBitrate: f.averageBitrate || f.average_bitrate,
334+
quality: f.quality,
335+
qualityLabel: f.qualityLabel || f.quality_label,
336+
audioTrackId: f.audioTrack?.id,
337+
approxDurationMs: f.approxDurationMs || f.approx_duration_ms,
338+
contentLength: f.contentLength || f.content_length,
339+
isDrc: !!f.isDrc
340+
}))
341+
342+
return {
343+
protocol: 'sabr',
344+
url: serverAbrUrl,
345+
additionalData: {
346+
serverAbrStreamingUrl: serverAbrUrl,
347+
videoPlaybackUstreamerConfig: ustreamerConfig,
348+
visitorData: this.getClient(context).client.visitorData,
349+
clientInfo: { clientName: 3, clientVersion: '20.51.39' },
350+
formats,
351+
accessToken: null,
352+
userAgent: this.getClient(context).client.userAgent
353+
}
354+
}
355+
}
356+
317357
return await this._extractStreamData(
318358
playerResponse,
319359
decodedTrack,

src/sources/youtube/clients/Web.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import {
55
buildTrack,
66
checkURLType
77
} from '../common.js'
8-
import { PoTokenManager } from '../potoke.js'
8+
import { poTokenManager } from '../potoke.js'
99

1010
export default class Web extends BaseClient {
1111
constructor(nodelink, oauth) {
1212
super(nodelink, 'WEB', oauth)
13-
this.poTokenManager = new PoTokenManager()
13+
this.poTokenManager = poTokenManager
1414
}
1515

1616
getClient(context) {
@@ -255,7 +255,7 @@ export default class Web extends BaseClient {
255255
}
256256

257257
const { poToken, visitorData } = await this.poTokenManager.generate(decodedTrack.identifier)
258-
258+
259259
if (poToken) {
260260
const client = this.getClient(context)
261261
client.client.visitorData = visitorData
@@ -288,7 +288,7 @@ export default class Web extends BaseClient {
288288

289289
try {
290290
const { body: playerResponse } = await makeRequest(
291-
'https://youtubei.googleapis.com/youtubei/v1/player?prettyPrint=false',
291+
'https://youtubei.googleapis.com/youtubei/v1/player?prettyPrint=false',
292292
{
293293
method: 'POST',
294294
headers: {
@@ -310,7 +310,7 @@ export default class Web extends BaseClient {
310310

311311
if (serverAbrUrl) {
312312
const playerScript = await cipherManager.getCachedPlayerScript()
313-
313+
314314
let resolvedUrl = serverAbrUrl
315315
if (playerScript) {
316316
try {

0 commit comments

Comments
 (0)