Skip to content

Commit

Permalink
fix(PlayerElement): WebAudio concurrency and cracks when switching tr…
Browse files Browse the repository at this point in the history
…acks

Signed-off-by: Fernando Fernández <[email protected]>
  • Loading branch information
ferferga committed Sep 7, 2024
1 parent 248afe0 commit 661107b
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 40 deletions.
107 changes: 68 additions & 39 deletions frontend/src/components/Playback/PlayerElement.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
crossorigin
playsinline
:loop="playbackManager.isRepeatingOnce"
:class="{ stretched: playerElement.isStretched.value }"
:class="{ 'uno-object-fill': playerElement.isStretched.value }"
@loadeddata="onLoadedData">
<track
v-for="sub in playbackManager.currentItemVttParsedSubtitleTracks"
Expand Down Expand Up @@ -43,14 +43,31 @@ import { getImageInfo } from '@/utils/images';
import { isNil } from '@/utils/validation';
const { t } = useI18n();
let busyWebAudio = false;
const hls = Hls.isSupported()
? new Hls({
testBandwidth: false,
workerPath: HlsWorkerUrl
})
: undefined;
const mediaElementType = computed<'audio' | 'video' | undefined>(() => {
if (playbackManager.isAudio) {
return 'audio';
} else if (playbackManager.isVideo) {
return 'video';
}
});
const posterUrl = computed(() =>
!isNil(playbackManager.currentItem)
&& playbackManager.isVideo
? getImageInfo(playbackManager.currentItem, {
preferBackdrop: true
}).url
: undefined
);
/**
* Detaches HLS instance after playback is done
*/
Expand All @@ -65,30 +82,56 @@ function detachHls(): void {
* Suspends WebAudio when no playback is in place
*/
async function detachWebAudio(): Promise<void> {
if (mediaWebAudio.sourceNode) {
mediaWebAudio.sourceNode.disconnect();
mediaWebAudio.sourceNode = undefined;
}
if (mediaWebAudio.context.state === 'running' && !busyWebAudio) {
busyWebAudio = true;
try {
if (mediaWebAudio.gainNode) {
mediaWebAudio.gainNode.gain.setValueAtTime(mediaWebAudio.gainNode.gain.value, mediaWebAudio.context.currentTime);
mediaWebAudio.gainNode.gain.exponentialRampToValueAtTime(0.0001, mediaWebAudio.context.currentTime + 1.5);
await nextTick();
await new Promise(resolve => window.setTimeout(resolve));
mediaWebAudio.gainNode.disconnect();
mediaWebAudio.gainNode = undefined;
}
await mediaWebAudio.context.suspend();
}
if (mediaWebAudio.sourceNode) {
mediaWebAudio.sourceNode.disconnect();
mediaWebAudio.sourceNode = undefined;
}
const mediaElementType = computed<'audio' | 'video' | undefined>(() => {
if (playbackManager.isAudio) {
return 'audio';
} else if (playbackManager.isVideo) {
return 'video';
await mediaWebAudio.context.suspend();
} catch {} finally {
busyWebAudio = false;
}
}
});
}
const posterUrl = computed(() =>
!isNil(playbackManager.currentItem)
&& playbackManager.isVideo
? getImageInfo(playbackManager.currentItem, {
preferBackdrop: true
}).url
: undefined
);
/**
* Resumes WebAudio when playback is in place
*/
async function attachWebAudio(el: HTMLMediaElement): Promise<void> {
if (mediaWebAudio.context.state === 'suspended' && !busyWebAudio) {
busyWebAudio = true;
try {
await mediaWebAudio.context.resume();
mediaWebAudio.sourceNode = mediaWebAudio.context.createMediaElementSource(el);
mediaWebAudio.sourceNode.connect(mediaWebAudio.context.destination);
/**
* The gain node is to avoid cracks when stopping playback or switching really fast between tracks
*/
mediaWebAudio.gainNode = mediaWebAudio.context.createGain();
mediaWebAudio.gainNode.connect(mediaWebAudio.context.destination);
mediaWebAudio.gainNode.gain.setValueAtTime(mediaWebAudio.gainNode.gain.value, mediaWebAudio.context.currentTime);
mediaWebAudio.gainNode.gain.exponentialRampToValueAtTime(1, mediaWebAudio.context.currentTime + 1.5);
} catch {} finally {
busyWebAudio = false;
}
}
}
/**
* Called by the media element when the playback is ready
Expand Down Expand Up @@ -142,23 +185,15 @@ watch(mediaElementRef, async () => {
await detachWebAudio();
if (mediaElementRef.value) {
await nextTick();
if (mediaElementType.value === 'video' && hls) {
hls.attachMedia(mediaElementRef.value);
hls.on(Events.ERROR, onHlsEror);
}
await mediaWebAudio.context.resume();
mediaWebAudio.sourceNode = mediaWebAudio.context.createMediaElementSource(
mediaElementRef.value
);
mediaWebAudio.sourceNode.connect(mediaWebAudio.context.destination);
console.log('attach called');
await attachWebAudio(mediaElementRef.value);
}
/**
* Needed so WebAudio is properly disposed
*/
}, { flush: 'sync' });
});
watch(
() => playbackManager.currentSourceUrl,
Expand Down Expand Up @@ -193,9 +228,3 @@ watch(
}
);
</script>

<style scoped>
.stretched {
object-fit: fill;
}
</style>
3 changes: 2 additions & 1 deletion frontend/src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ export const mediaControls = useMediaControls(mediaElementRef);
*/
export const mediaWebAudio = {
context: new AudioContext(),
sourceNode: undefined as undefined | MediaElementAudioSourceNode
sourceNode: undefined as undefined | MediaElementAudioSourceNode,
gainNode: undefined as undefined | GainNode
};
/**
* Reactively tracks if the user wants animations (false) or not (true).
Expand Down

0 comments on commit 661107b

Please sign in to comment.