fix(playback): keep Video.js controls on the live MPEG-TS path (#1385)

Video.js was constructed without the controls option — the component relied
on a [controls] template binding on the original <video> element instead.
player.reset(), which every raw MPEG-TS/live source change goes through,
replaces the tech <video> element; the binding's target is disposed, so live
playback ended up with no native controls and a vjs-controls-disabled control
bar: no visible controls at all.

Enable controls through the Video.js constructor options in legacy mode and
drop the template binding. The Video.js control bar is a player-level child
that survives loadTech_, so it stays across resets — and the quality selector
and aspect-ratio panel buttons it hosts become reachable again.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 authored and GitHub committed 2026-08-08 21:43:05 +02:00
1 parent f58460380f
commit c95f826739
5 files changed
+39 -3

No files matched your search

@@ -19,9 +19,29 @@ describe('Video.js player setup', () => {
expect(createVjsPlayerOptions(options, false, false)).toEqual({
...options,
autoplay: true,
controls: true,
});
});
it('keeps Video.js controls enabled on the raw MPEG-TS legacy path', () => {
// player.reset() replaces the tech <video>, so the control bar must
// come from the constructor option — a template binding on the
// original element cannot survive the swap.
expect(
createVjsPlayerOptions(
{ sources: [{ src: 'https://example.test/live.ts' }] },
true,
false
)
).toEqual(
expect.objectContaining({
sources: [],
autoplay: false,
controls: true,
})
);
});
it('removes duplicate Video.js interaction ownership for shared controls', () => {
expect(
createVjsPlayerOptions(
@@ -13,7 +13,12 @@ export function createVjsPlayerOptions(
? { ...options, sources: [], autoplay: false }
: { ...options, autoplay: true };
if (!sharedControls) {
return baseOptions;
// Controls must be enabled through the constructor, not a `[controls]`
// template binding: `player.reset()` (raw MPEG-TS/live path) replaces
// the tech <video> element, and a binding to the original element can
// never reach the replacement — Video.js's own control bar lives
// outside the tech element and survives the swap.
return { ...baseOptions, controls: true };
}
return {
...baseOptions,
@@ -2,7 +2,6 @@
<video
#target
class="video-js"
[controls]="!sharedControls"
muted
playsinline
preload="none"
@@ -78,13 +78,16 @@ describe('VjsPlayerComponent', () => {
expect.any(Element),
expect.objectContaining({
autoplay: true,
controls: true,
userActions: { hotkeys: true },
spatialNavigation: { enabled: true },
}),
expect.any(Function)
);
// Native controls stay off: Video.js owns the control bar, and the
// bar has to survive the tech-element swap in player.reset().
expect(fixture.nativeElement.querySelector('video').controls).toBe(
true
false
);
expect(
fixture.nativeElement.querySelector('app-player-controls')