Skip to content

Commit

Permalink
feat: Player (#135)
Browse files Browse the repository at this point in the history
  • Loading branch information
matvp91 authored Dec 19, 2024
1 parent 0040d16 commit adb6760
Show file tree
Hide file tree
Showing 95 changed files with 1,870 additions and 3,190 deletions.
Binary file modified bun.lockb
Binary file not shown.
61 changes: 61 additions & 0 deletions fixtures/media-chrome/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<!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;
}

media-controller[interstitial="1"] media-time-range {
display: none;
}
</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/6c127f2a-ca33-4eca-aff2-29ab3c3aac7c/master.m3u8">
</superstreamer-video>
<media-loading-indicator slot="centered-chrome" noautohide></media-loading-indicator>
<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 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>
283 changes: 283 additions & 0 deletions fixtures/media-chrome/superstreamer-video-element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
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;

#video;

constructor() {
super();

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

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

this.#video = document.createElement("video");

this.#bindListeners();
}

#bindListeners() {
this.#player.on(Events.PLAYHEAD_CHANGE, () => {
switch (this.#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;
}
});

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

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

this.#player.on(Events.SEEKING_CHANGE, () => {
if (this.#player.seeking) {
this.dispatchEvent(new Event("seeking"));
} else {
this.dispatchEvent(new Event("seeked"));
}
});

this.#player.on(Events.READY, async () => {
this.#readyState = 1;

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.#player.on(Events.STARTED, () => {
this.#readyState = 3;
});

this.#player.on(Events.ASSET_CHANGE, () => {
const controller = this.closest("media-controller");
if (controller) {
controller.setAttribute("interstitial", this.#player.asset ? "1" : "0");
}
});
}

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

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

get textTracks() {
return this.#video.textTracks;
}

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

async load() {
this.#readyState = 0;

while (this.#video.firstChild) {
this.#video.firstChild.remove();
}

for (const videoTrack of this.videoTracks) {
this.removeVideoTrack(videoTrack);
}
for (const audioTrack of this.audioTracks) {
this.removeAudioTrack(audioTrack);
}

this.#player.unload();

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

if (this.src) {
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();
await Promise.resolve();
}

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 track = [...this.audioTracks].find((a) => a.enabled);
if (track) {
const id = track[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 track = [...this.textTracks].find((t) => t.mode === "showing");
if (track) {
const id = track[symbolTrackId_];
this.#player.setSubtitleTrack(id);
} else {
this.#player.setSubtitleTrack(null);
}
});
}

addTextTrack(kind, label, language) {
const trackEl = document.createElement("track");
trackEl.kind = kind;
trackEl.label = label;
trackEl.srclang = language;
trackEl.track.mode = "hidden";
this.#video.append(trackEl);
return trackEl.track;
}
}

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

export default SuperstreamerVideoElement;
1 change: 1 addition & 0 deletions packages/app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Superstreamer</title>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
<div id="root"></div>
Expand Down
1 change: 0 additions & 1 deletion packages/app/src/components/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ export function CodeEditor({

return (
<MonacoEditor
className="h-full w-full"
defaultLanguage="json"
defaultValue={value}
onMount={onMount}
Expand Down
22 changes: 0 additions & 22 deletions packages/app/src/components/DataDump.tsx

This file was deleted.

Loading

0 comments on commit adb6760

Please sign in to comment.