Skip to content
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

Player #135

Merged
merged 31 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
39c10a7
player
matvp91 Dec 3, 2024
a68b72c
Merge branch 'main' of github.com:matvp91/mixwave into feature/player…
matvp91 Dec 5, 2024
01c553c
Multiple video elements
matvp91 Dec 5, 2024
ca3e053
New player
matvp91 Dec 6, 2024
98e094b
Fixture for media-chrome
matvp91 Dec 6, 2024
45dde29
Prettier
matvp91 Dec 6, 2024
d69fdf1
Rendition menu
matvp91 Dec 6, 2024
c464967
Added mapping for audio and quality
matvp91 Dec 6, 2024
df5fe0d
Added expiry
matvp91 Dec 7, 2024
cbebc18
Removed unused log
matvp91 Dec 7, 2024
f6a7e6a
Added temp code
matvp91 Dec 9, 2024
fa5d6ac
Added textTracks support
matvp91 Dec 9, 2024
c99d615
Ignore error
matvp91 Dec 9, 2024
0e92824
Interstitial demo asset
matvp91 Dec 9, 2024
621f243
Fix tags
matvp91 Dec 10, 2024
8ef5324
Seeking
matvp91 Dec 10, 2024
61b32ae
Added support for multiple load calls
matvp91 Dec 10, 2024
bfd4a8d
Added interstitial asset
matvp91 Dec 10, 2024
c8f7c6d
Hide seekbar
matvp91 Dec 10, 2024
03ab89c
Merge branch 'main' of github.com:matvp91/mixwave into feature/player…
matvp91 Dec 10, 2024
a0f4ae7
Added object dump
matvp91 Dec 10, 2024
73fc260
Added context viewer
matvp91 Dec 11, 2024
f2cf130
Added json viewer
matvp91 Dec 11, 2024
da06384
Support for text files
matvp91 Dec 11, 2024
158412f
Merge branch 'main' of github.com:matvp91/mixwave into feature/player…
matvp91 Dec 14, 2024
197018c
Added interstitial instead of solely asset
matvp91 Dec 14, 2024
b58975a
Added player controls
matvp91 Dec 17, 2024
930d5fb
Added controls to bottom
matvp91 Dec 17, 2024
62a4146
Show cuepoints
matvp91 Dec 17, 2024
bb23744
Added player view
matvp91 Dec 17, 2024
b18b0b1
Added more player
matvp91 Dec 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified bun.lockb
Binary file not shown.
57 changes: 57 additions & 0 deletions fixtures/media-chrome/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css"
>
<style>
media-controller {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
background: #000;
}
</style>
</head>
<body>
<script async src="https://cdn.jsdelivr.net/npm/es-module-shims"></script>
<script type="importmap">
{
"imports": {
"super-media-element": "https://cdn.jsdelivr.net/npm/super-media-element@1.3/+esm",
"media-tracks": "https://cdn.jsdelivr.net/npm/media-tracks@0.2/+esm",
"@superstreamer/player": "/packages/player/dist/index.js",
"hls.js": "https://cdn.jsdelivr.net/npm/hls.js@1.6.0-beta.1/dist/hls.mjs"
}
}
</script>
<script type="module" src="./superstreamer-video-element.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/media-chrome/+esm"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/media-chrome/menu/+esm"></script>

<media-controller>
<superstreamer-video
slot="media"
src="https://stitcher.superstreamer.xyz/session/8121e4cc-2024-45b5-bfc8-323dec368819/master.m3u8">
</superstreamer-video>
<media-loading-indicator slot="centered-chrome" no-auto-hide></media-loading-indicator>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<media-loading-indicator slot="centered-chrome" no-auto-hide></media-loading-indicator>
<media-loading-indicator slot="centered-chrome" noautohide></media-loading-indicator>

I was wondering where you got this markup? seems to have the old way of kebab cased attributes.
the new attributes in Media Chrome are all without dashes in them.

<media-rendition-menu hidden anchor="auto"></media-rendition-menu>
<media-audio-track-menu hidden anchor="auto"></media-audio-track-menu>
<media-captions-menu hidden anchor="auto"></media-captions-menu>
<media-control-bar>
<media-play-button></media-play-button>
<media-seek-forward-button></media-seek-forward-button>
<media-mute-button></media-mute-button>
<media-volume-range></media-volume-range>
<media-time-range></media-time-range>
<media-time-display show-duration></media-time-display>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<media-time-display show-duration></media-time-display>
<media-time-display showduration></media-time-display>

<media-rendition-menu-button></media-rendition-menu-button>
<media-audio-track-menu-button></media-audio-track-menu-button>
<media-captions-menu-button></media-captions-menu-button>
<media-fullscreen-button></media-fullscreen-button>
</media-control-bar>
</media-controller>
</body>
</html>
234 changes: 234 additions & 0 deletions fixtures/media-chrome/superstreamer-video-element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import { Events, HlsPlayer } from "@superstreamer/player";
import { MediaTracksMixin } from "media-tracks";

function getTemplateHTML() {
return `
<style>
:host {
width: 100%;
height: 100%;
}
</style>
<div class="container"></div>
`;
}

const SymbolTrackId = Symbol("superstreamer.trackId");

class SuperstreamerVideoElement extends MediaTracksMixin(
globalThis.HTMLElement,
) {
static getTemplateHTML = getTemplateHTML;

static shadowRootOptions = {
mode: "open",
};

static observedAttributes = ["src"];

#player;

#readyState = 0;

constructor() {
super();

const video = document.createElement("video");

this.textTracks = video.textTracks;
this.addTextTrack = video.addTextTrack.bind(video);
}

get src() {
return this.getAttribute("src");
}

set src(val) {
if (this.src === val) {
return;
}
this.setAttribute("src", val);
}

attributeChangedCallback(attrName, oldValue, newValue) {
if (attrName === "src" && oldValue !== newValue) {
this.load();
}
}

load() {
if (!this.shadowRoot) {
this.attachShadow({
mode: "open",
});
this.shadowRoot.innerHTML = getTemplateHTML();
}

this.#readyState = 0;
this.dispatchEvent(new Event("emptied"));

if (!this.#player) {
const container = this.shadowRoot.querySelector(".container");
const player = (this.#player = new HlsPlayer(container));

// TODO: For debug purposes.
Object.assign(window, { player });

player.on(Events.PLAYHEAD_CHANGE, () => {
switch (player.playhead) {
case "play":
this.dispatchEvent(new Event("play"));
break;
case "playing":
this.dispatchEvent(new Event("playing"));
break;
case "pause":
this.dispatchEvent(new Event("pause"));
break;
}
});

player.on(Events.TIME_CHANGE, () => {
this.dispatchEvent(new Event("timeupdate"));
});

player.on(Events.VOLUME_CHANGE, () => {
this.dispatchEvent(new Event("volumechange"));
});

player.on(Events.READY, async () => {
this.dispatchEvent(new Event("loadedmetadata"));
this.dispatchEvent(new Event("durationchange"));
this.dispatchEvent(new Event("volumechange"));
this.dispatchEvent(new Event("loadcomplete"));

this.#createVideoTracks();
this.#createAudioTracks();
this.#createTextTracks();

this.#readyState = 1;
});

player.on(Events.STARTED, () => {
this.#readyState = 3;
});
}

this.dispatchEvent(new Event("loadstart"));

this.#player.load(this.src);
}

get currentTime() {
return this.#player.time;
}

set currentTime(val) {
this.#player.seekTo(val);
}

get duration() {
return this.#player.duration;
}

get paused() {
const { playhead } = this.#player;
if (playhead === "play" || playhead === "playing") {
return false;
}
return true;
}

get readyState() {
return this.#readyState;
}

get muted() {
return this.#player.volume === 0;
}

set muted(val) {
this.#player.setVolume(val ? 0 : 1);
}

get volume() {
return this.#player.volume;
}

set volume(val) {
this.#player.setVolume(val);
}

async play() {
this.#player.playOrPause();
}

pause() {
this.#player.playOrPause();
}

#createVideoTracks() {
let videoTrack = this.videoTracks.getTrackById("main");

if (!videoTrack) {
videoTrack = this.addVideoTrack("main");
videoTrack.id = "main";
videoTrack.selected = true;
}

this.#player.qualities.forEach((quality) => {
videoTrack.addRendition(
undefined,
quality.height,
quality.height,
undefined,
undefined,
);
});

this.videoRenditions.addEventListener("change", (event) => {
if (event.target.selectedIndex < 0) {
this.#player.setQuality(null);
} else {
const rendition = this.videoRenditions[event.target.selectedIndex];
this.#player.setQuality(rendition.height);
}
});
}

#createAudioTracks() {
this.#player.audioTracks.forEach((a) => {
const audioTrack = this.addAudioTrack("main", a.label, a.label);
audioTrack[SymbolTrackId] = a.id;
audioTrack.enabled = a.active;
});

this.audioTracks.addEventListener("change", () => {
const id = [...this.audioTracks].find((a) => a.enabled)?.[SymbolTrackId];
this.#player.setAudioTrack(id);
});
}

#createTextTracks() {
this.#player.subtitleTracks.forEach((s) => {
const textTrack = this.addTextTrack("subtitles", s.label, s.track.lang);
textTrack[SymbolTrackId] = s.id;
});

this.textTracks.addEventListener("change", () => {
const id =
[...this.textTracks].find((t) => t.mode === "showing")?.[SymbolTrackId] ??
null;
this.#player.setSubtitleTrack(id);
});
}
}

if (!globalThis.customElements?.get("superstreamer-video")) {
globalThis.customElements.define(
"superstreamer-video",
SuperstreamerVideoElement,
);
}

export default SuperstreamerVideoElement;
55 changes: 18 additions & 37 deletions packages/app/src/components/Player.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,29 @@
import {
ControllerProvider,
Controls,
useController,
} from "@superstreamer/player/react";
import Hls from "hls.js";
import { useEffect, useState } from "react";
import type { Lang, Metadata } from "@superstreamer/player/react";
import { HlsPlayer } from "@superstreamer/player";
import { useEffect, useRef, useState } from "react";

interface PlayerProps {
url?: string | null;
metadata: Metadata;
lang: Lang;
}

export function Player({ url, lang, metadata }: PlayerProps) {
const [hls] = useState(() => new Hls());
const controller = useController(hls, {
multipleVideoElements: false,
});
export function Player({ url }: PlayerProps) {
const ref = useRef<HTMLDivElement>(null);
const [player, setPlayer] = useState<HlsPlayer | null>(null);

useEffect(() => {
if (url) {
hls.loadSource(url);
}
}, [url]);
const player = new HlsPlayer(ref.current!);
Object.assign(window, { player });
setPlayer(player);
}, []);

useEffect(() => {
Object.assign(window, {
facade: controller.facade,
});
}, [controller]);
if (!player || !url) {
return;
}
player.load(url);
return () => {
player.unload();
};
}, [player, url]);

return (
<ControllerProvider controller={controller}>
<div
className="relative aspect-video bg-black overflow-hidden rounded-md"
data-sprs-container
>
<video
ref={controller.mediaRef}
className="absolute inset-O w-full h-full"
/>
<Controls lang={lang} metadata={metadata} />
</div>
</ControllerProvider>
);
return <div className="relative aspect-video bg-black" ref={ref} />;
}
2 changes: 1 addition & 1 deletion packages/app/src/routes/(dashboard)/_layout/player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function RouteComponent() {
return (
<div className="h-screen p-8 flex gap-4">
<div className="grow">
<Player url={url} lang="eng" metadata={{}} />
<Player url={url} />
<Card className="mt-4 p-4">
<Form
ref={formRef}
Expand Down
Loading