Fix infinite non-settle when client closes the stream

This commit is contained in:
Chocobozzz
2026-07-13 14:01:17 +02:00
parent 3ab630ef38
commit c8ef2bf95f
+36 -14
View File
@@ -30,6 +30,7 @@ export class VideoDownload {
private readonly video: MVideoThumbnails
private readonly videoFiles: MVideoFile[]
private cleanupPromise: Promise<void>
private allowDirectSending = true
constructor (options: {
@@ -45,15 +46,19 @@ export class VideoDownload {
bytesPerIpPerSecond: number
ip: string
}) {
return new Promise<void>(async (res, rej) => {
const totalBytesPerSecond = options?.totalBytesPerSecond
const bytesPerIpPerSecond = options?.bytesPerIpPerSecond
const ip = options?.ip
try {
let rejectOnStreamError: (err: Error) => void
const streamErrorPromise = new Promise<never>((_, rej) => {
rejectOnStreamError = rej
})
const run = async () => {
VideoDownload.totalDownloads++
const maxResolution = await this.buildMuxInputs(rej)
const maxResolution = await this.buildMuxInputs(rejectOnStreamError)
// Include cover to audio file?
const { coverPath, isTmpDestination } = maxResolution === 0
@@ -76,10 +81,20 @@ export class VideoDownload {
? new ThrottleStream({ totalBytesPerSecond, bytesPerIpPerSecond, ip })
: new PassThrough()
try {
await pipeline(input, throttleStream, output)
} catch (err) {
if ((err?.message || '').includes('Output stream closed')) {
logger.info(`Client aborted direct download for video ${this.video.url}`, lTags(this.video.uuid))
return
}
throw err
}
return
}
res()
} else {
logger.info(`Muxing files for video ${this.video.url}`, { inputs: this.inputsToLog(), ...lTags(this.video.uuid) })
this.ffmpegContainer = new FFmpegContainer(getFFmpegCommandWrapperOptions('vod'))
@@ -110,8 +125,6 @@ export class VideoDownload {
])
logger.info(`Mux ended for video ${this.video.url}`, { inputs: this.inputsToLog(), ...lTags(this.video.uuid) })
res()
} catch (err) {
const message = err?.message || ''
@@ -128,16 +141,19 @@ export class VideoDownload {
throw err
} finally {
this.ffmpegContainer.forceKill()
// cleanup() may already have force killed and unset ffmpegContainer if a stream errored and won the race below
this.ffmpegContainer?.forceKill()
}
}
} catch (err) {
rej(err)
try {
// If a stream errors, don't wait for run() to notice: reject immediately so the caller isn't stuck
// run() keeps executing in the background but will abort once cleanup() destroys its streams/ffmpeg process
await Promise.race([ run(), streamErrorPromise ])
} finally {
this.cleanup()
.catch(cleanupErr => logger.error('Cannot cleanup after mux error', { err: cleanupErr, ...lTags(this.video.uuid) }))
}
})
}
// ---------------------------------------------------------------------------
@@ -167,9 +183,7 @@ export class VideoDownload {
...lTags(this.video.uuid)
})
this.cleanup()
.catch(cleanupErr => logger.error('Cannot cleanup after mux error', { err: cleanupErr, ...lTags(this.video.uuid) }))
// cleanup() is already run by muxToMergeVideoFiles's finally block once this rejection wins the race
rej(err)
}
)
@@ -290,6 +304,14 @@ export class VideoDownload {
}
private async cleanup () {
if (this.cleanupPromise === undefined) {
this.cleanupPromise = this._doCleanup()
}
return this.cleanupPromise
}
private async _doCleanup () {
VideoDownload.totalDownloads--
for (const destination of this.tmpDestinations) {