-
Notifications
You must be signed in to change notification settings - Fork 24
Fix a "black screen flash" which occurs sometimes when restarting a track #111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9f77416
c52bff6
38bba10
0759f27
d00fdba
6f1d1f3
fc0ce33
4bff928
15ad53a
8352d14
4071561
80aae2d
b0e5fa1
a7d842e
68ddfee
45879a6
82f0f2d
9658ec0
367d72a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@livekit/track-processors': patch | ||
| --- | ||
|
|
||
| Fix a "black screen flash" which occurs sometimes when restarting the processor wrapper |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,10 @@ | ||
| import { type ProcessorOptions, type Track, type TrackProcessor } from 'livekit-client'; | ||
| import { TrackTransformer } from './transformers'; | ||
| import { TrackTransformer, TrackTransformerDestroyOptions } from './transformers'; | ||
| import { createCanvas, waitForTrackResolution } from './utils'; | ||
| import { LoggerNames, getLogger } from './logger'; | ||
|
|
||
| type ProcessorWrapperLifecycleState = 'idle' | 'initializing' | 'running' | 'media-exhausted' | 'destroying' | 'destroyed'; | ||
|
|
||
| export interface ProcessorWrapperOptions { | ||
|
Comment on lines
5
to
8
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| /** | ||
| * Maximum frame rate for fallback canvas.captureStream implementation | ||
|
|
@@ -81,10 +83,10 @@ export default class ProcessorWrapper< | |
| // FPS control for fallback implementation | ||
| private maxFps: number; | ||
|
|
||
| private symbol?: Symbol; | ||
|
|
||
| private log = getLogger(LoggerNames.ProcessorWrapper); | ||
|
|
||
| private lifecycleState: ProcessorWrapperLifecycleState = 'idle'; | ||
|
|
||
| constructor( | ||
| transformer: Transformer, | ||
| name: string, | ||
|
|
@@ -146,6 +148,8 @@ export default class ProcessorWrapper< | |
| } | ||
|
|
||
| async init(opts: ProcessorOptions<Track.Kind>): Promise<void> { | ||
| this.log.debug('Init called'); | ||
| this.lifecycleState = 'initializing'; | ||
| await this.setup(opts); | ||
|
|
||
| if (!this.canvas) { | ||
|
|
@@ -162,6 +166,7 @@ export default class ProcessorWrapper< | |
| } else { | ||
| this.initStreamProcessorPath(); | ||
| } | ||
| this.lifecycleState = 'running'; | ||
| } | ||
|
|
||
| private initStreamProcessorPath() { | ||
|
|
@@ -174,20 +179,20 @@ export default class ProcessorWrapper< | |
| const readableStream = this.processor.readable; | ||
| const pipedStream = readableStream.pipeThrough(this.transformer!.transformer!); | ||
|
|
||
| const symbol = Symbol('stream'); | ||
| this.symbol = symbol; | ||
|
|
||
| pipedStream | ||
| .pipeTo(this.trackGenerator.writable) | ||
| // destroy processor if stream finishes | ||
| .then(() => this.destroy(symbol)) | ||
| // if stream finishes, the media to process is exhausted | ||
| .then(() => this.handleMediaExhausted()) | ||
| // destroy processor if stream errors - unless it's an abort error | ||
| .catch((e) => { | ||
| if (e instanceof DOMException && e.name === 'AbortError') { | ||
| this.log.log('stream processor path aborted'); | ||
| } else if (e instanceof DOMException && e.name === 'InvalidStateError' && e.message === 'Stream closed') { | ||
| this.log.log('stream processor underlying stream closed'); | ||
| this.handleMediaExhausted(); | ||
| } else { | ||
| this.log.error('error when trying to pipe', e); | ||
| this.destroy(symbol); | ||
| this.destroy(); | ||
| } | ||
| }); | ||
|
|
||
|
|
@@ -241,6 +246,7 @@ export default class ProcessorWrapper< | |
|
|
||
| private startRenderLoop() { | ||
| if (!this.sourceDummy || !(this.sourceDummy instanceof HTMLVideoElement)) { | ||
| this.handleMediaExhausted(); | ||
| return; | ||
| } | ||
|
|
||
|
|
@@ -263,6 +269,7 @@ export default class ProcessorWrapper< | |
| !this.sourceDummy || | ||
| !(this.sourceDummy instanceof HTMLVideoElement) | ||
| ) { | ||
| this.handleMediaExhausted(); | ||
| return; | ||
| } | ||
|
|
||
|
|
@@ -349,7 +356,8 @@ export default class ProcessorWrapper< | |
| } | ||
|
|
||
| async restart(opts: ProcessorOptions<Track.Kind>): Promise<void> { | ||
| await this.destroy(); | ||
| this.log.debug('Restart called'); | ||
| await this.destroy({ willProcessorRestart: true }); | ||
| await this.init(opts); | ||
| } | ||
|
|
||
|
|
@@ -362,11 +370,18 @@ export default class ProcessorWrapper< | |
| await this.transformer.update(options[0]); | ||
| } | ||
|
|
||
| async destroy(symbol?: Symbol) { | ||
| if (symbol && this.symbol !== symbol) { | ||
| // If the symbol is provided, we only destroy if it matches the current symbol | ||
| /** Called if the media pipeline no longer can read frames to process from the source media */ | ||
| private async handleMediaExhausted() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice! |
||
| this.log.debug('Media was exhausted from source'); | ||
| if (this.lifecycleState !== 'running') { | ||
| return; | ||
| } | ||
| this.lifecycleState = 'media-exhausted' | ||
| await this.cleanup(); | ||
| } | ||
|
|
||
| /** Tears down the media stack logic initialized in initStreamProcessorPath / initFallbackPath */ | ||
| private async cleanup() { | ||
| if (this.useStreamFallback) { | ||
| this.processingEnabled = false; | ||
| if (this.animationFrameId) { | ||
|
|
@@ -378,9 +393,28 @@ export default class ProcessorWrapper< | |
| } | ||
| this.capturedStream?.getTracks().forEach((track) => track.stop()); | ||
| } else { | ||
| // NOTE: closing writableControl below terminates the stream in initStreamProcessorPath / | ||
| // calls the .then(...) which calls this.handleMediaExhausted | ||
| await this.processor?.writableControl?.close(); | ||
| this.trackGenerator?.stop(); | ||
| } | ||
| await this.transformer.destroy(); | ||
| } | ||
|
|
||
| async destroy(transformerDestroyOptions: TrackTransformerDestroyOptions = { willProcessorRestart: false }) { | ||
| this.log.debug(`Destroy called - lifecycleState=${this.lifecycleState}, transformerDestroyOptions=${JSON.stringify(transformerDestroyOptions)}`); | ||
| switch (this.lifecycleState) { | ||
| case 'running': | ||
| case 'media-exhausted': | ||
| this.lifecycleState = 'destroying'; | ||
|
|
||
| await this.cleanup(); | ||
|
|
||
| await this.transformer.destroy(transformerDestroyOptions); | ||
| this.lifecycleState = 'destroyed'; | ||
| break; | ||
|
|
||
| default: | ||
| break; | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added this UI control in the example to change the initial state the background processor is added in: