fix(player): a stale session answer can no longer stop the playback that won
- The engine tracks the request out per item: A, then B, then back to A sent a second request for A beside the one still out. - A hardware transcode superseded during its fallback answered with the NEWER session; the losing request's player then stopped it as stale. It now answers with its own (dead) session. - A replaced hls.js instance's late manifest no longer marks the player ready or plays the outgoing media. - A pause given after the answer stands: the manifest and the first canplay read the live intent. - The debounced A/V-offset restart is dropped on an item change instead of restarting the previous item under the next one's title.
This commit is contained in:
@@ -164,7 +164,9 @@ export class HlsEngine {
|
||||
private attachedId: string | null = null;
|
||||
// The attached session's play intent, which stands until it is ready (see `wantsPlay`).
|
||||
private attachedIntent: Intent | null = null;
|
||||
private inFlight: Request | null = null;
|
||||
// The request out for each item. Per item, not one slot: another item's request took the slot, and
|
||||
// the way back to the first item then sent a second request beside the one still out for it.
|
||||
private readonly inFlight = new Map<string, Request>();
|
||||
private queued: Request | null = null;
|
||||
// How often a session the server answered 404 for may be reclaimed (see `onHlsError`).
|
||||
private reclaims = { n: 0, at: 0 };
|
||||
@@ -275,13 +277,13 @@ export class HlsEngine {
|
||||
// stale "Preparing stream…" for a session not known yet — set honestly from the answer.
|
||||
this.set({ ready: false, error: null, transcode: false });
|
||||
const req: Request = { id, startAt, intent, gen };
|
||||
if (this.inFlight && this.inFlight.id === id) this.queued = req;
|
||||
if (this.inFlight.has(id)) this.queued = req;
|
||||
else this.send(req);
|
||||
}
|
||||
|
||||
private send(req: Request): void {
|
||||
this.queued = null;
|
||||
this.inFlight = req;
|
||||
this.inFlight.set(req.id, req);
|
||||
void this.deps.requestSession(req.id, req.startAt, this.deps.params()).then(
|
||||
(sess) => this.answered(req, sess),
|
||||
(e: unknown) => this.failed(req, e),
|
||||
@@ -295,9 +297,9 @@ export class HlsEngine {
|
||||
/** The in-flight request settled: the queued one goes now, unless it is stale too (the viewer
|
||||
* left the item, or the player closed). */
|
||||
private settledInFlight(req: Request): void {
|
||||
if (this.inFlight === req) this.inFlight = null;
|
||||
if (this.inFlight.get(req.id) === req) this.inFlight.delete(req.id);
|
||||
const next = this.queued;
|
||||
if (!next || this.inFlight) return;
|
||||
if (!next || this.inFlight.has(next.id)) return;
|
||||
this.queued = null;
|
||||
if (this.alive && next.gen === this.gen) this.send(next);
|
||||
}
|
||||
@@ -345,7 +347,7 @@ export class HlsEngine {
|
||||
// Belt and braces: the play() on MANIFEST_PARSED / loadedmetadata can be rejected when the media
|
||||
// is not ready yet (slow first segments over a proxy), leaving the video paused after a seek the
|
||||
// viewer was watching through. Retried once the element can play; the intent is read when it
|
||||
// fires, since the viewer may have asked for play or pause meanwhile.
|
||||
// fires, since the viewer may have asked for play or pause meanwhile (`requestPlay` keeps it).
|
||||
this.listen(media, req, "canplay", () => {
|
||||
if (intent.play && media.paused) media.play().catch(() => {});
|
||||
});
|
||||
@@ -397,9 +399,14 @@ export class HlsEngine {
|
||||
|
||||
private hlsHandlers(req: Request, media: HTMLMediaElement): HlsHandlers {
|
||||
return {
|
||||
// Only the current session's: an instance replaced before its manifest parsed (a restart asked
|
||||
// for right after an attach) marked the player ready while the next session was still being
|
||||
// asked for — so that one then attached past `wantsPlay`'s not-ready guard — and played the
|
||||
// outgoing media.
|
||||
onManifestParsed: () => {
|
||||
if (!this.current(req)) return;
|
||||
this.set({ ready: true });
|
||||
if (this.alive && req.intent.play) media.play().catch(() => {});
|
||||
if (req.intent.play) media.play().catch(() => {});
|
||||
},
|
||||
// The selected audio is restored once hls.js has the renditions: on MANIFEST_PARSED the list
|
||||
// was not populated yet, the choice was dropped, and the default track played under the
|
||||
@@ -509,6 +516,9 @@ export class HlsEngine {
|
||||
if (!want && !media.paused) media.pause();
|
||||
return true;
|
||||
}
|
||||
// The attached session's intent follows: its manifest and first canplay still play by it, and a
|
||||
// pause given before they fired was played over.
|
||||
if (this.attachedIntent) this.attachedIntent.play = want;
|
||||
if (want) media.play().catch(() => {});
|
||||
else media.pause();
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user