view src/VideoPage.ts @ 49:1bd17c2e5517 default tip

video.py must sign video urls for serve-files.js to serve them
author drewp@bigasterisk.com
date Fri, 06 Dec 2024 17:13:51 -0800
parents 046673b1cc24
children
line wrap: on
line source

import { Router } from "@lit-labs/router";
import { LitElement, PropertyValues, TemplateResult, css, html, unsafeCSS } from "lit";
import { customElement, property, queryAsync } from "lit/decorators.js";
import { PagePlayer, ShakaVideoElement } from "./PagePlayer";
import maincss from "./main.css?inline";
export { SlProgressBar } from "@shoelace-style/shoelace";
export { PagePlayer } from "./PagePlayer";
export { VBreadcrumbs } from "./VBreadcrumbs";
export { VideoSection } from "./VideoSection";

interface VideoFile {
  webRelPath: string;
  webDataPath: string;
  label: string;
}

interface AutoplayVideoFile extends VideoFile {
  sig: string;
}

interface Subdir {
  label: string;
  path: string;
}

interface PageData {
  webDirRelPath: string;
  dirLabel: string;
  videos: VideoFile[];
  subdirs: Subdir[];
  autoplay: AutoplayVideoFile | null;
}

function subdirQuery(subdir: string): string {
  return "subdir=" + encodeURIComponent(subdir);
}
const PATH_PREFIX = "/video";
let routerShared: Router;

class route {
  path = PATH_PREFIX + "/*";
  pageData: PageData | null = null;

  async enter(params: { [key: string]: string | undefined }): Promise<boolean> {
    const webRelPath = "/" + params[0]!; // could be /a/dir/ or /a/video
    const resp = await fetch(PATH_PREFIX + "/api/videos?" + subdirQuery(webRelPath));
    if (resp.status == 404) {
      return false;
    }
    this.pageData = await resp.json();
    if (!this.pageData) return false;

    return true;
  }

  render(p: { [key: string]: string | undefined }) {
    return html`<video-page2 .pageData=${this.pageData}></video-page2>`;
  }
}

@customElement("video-page")
export class VideoPage extends LitElement {
  static styles = [unsafeCSS(maincss)];
  private _router = new Router(this, [new route()], {});

  constructor() {
    super();
    routerShared = this._router;
  }

  render() {
    const pd = (this._router.routes[0] as route).pageData;
    if (!pd) {
      return html`loading...`;
      throw new Error("no page data");
    }
    return html`
      <header>
        <img src="${PATH_PREFIX}/logo1.png" title="JelloBello" />
        <v-breadcrumbs dirPath=${pd.webDirRelPath}></v-breadcrumbs>
      </header>
      <main>${this._router.outlet()}</main>
      <footer>
        <bigast-loginbar></bigast-loginbar>
        <span><a href="ingest/">Add new videos...</a></span>
      </footer>
    `;
  }
}

@customElement("video-page2")
export class VideoPage2 extends LitElement {
  @property() pageData?: PageData;
  @queryAsync("page-player") pagePlayer!: Promise<PagePlayer>;

  protected firstUpdated(_changedProperties: PropertyValues): void {
    document.addEventListener("keydown", (e) => {
      if (e.key == "Escape") {
        this.gotoDirListingPage();
      }
    });
  }

  async connectedCallback() {
    super.connectedCallback();
    if (this.pageData?.autoplay) {
      this.startPlayer(this.pageData.autoplay);
      // this might not autoplay
    } else {
      this.closePlayer();
    }
  }
  protected update(changedProperties: PropertyValues): void {
    super.update(changedProperties);
    if (changedProperties.has("pageData")) {
      if (this.pageData?.autoplay) {
        this.startPlayer(this.pageData.autoplay);
      } else {
        this.closePlayer();
      }
    }
  }

  static styles = [
    unsafeCSS(maincss),
    css`
      :host {
        display: block;
      }
    `,
  ];

  thumbSrc(vf: VideoFile) {
    return PATH_PREFIX + "/api/thumbnail?webRelPath=" + encodeURIComponent(vf.webRelPath);
  }

  renderSubdir(subdir: Subdir): TemplateResult {
    return html`<a href="${PATH_PREFIX}${subdir.path}"><div class="subdir">${subdir.label}</div></a>`;
  }

  renderVideoListing(video: VideoFile) {
    return html`<video-section
      @playVideo=${this.gotoVideoPlayerPage}
      webRelPath=${video.webRelPath}
      thumbRelPath=${this.thumbSrc(video)}
      title="${video.label}"
    ></video-section>`;
  }

  render() {
    if (this.pageData == null) {
      return html`<div>Loading...</div>`;
    }
    const listings = [
      html`${this.pageData.subdirs.map((s) => this.renderSubdir(s))}`, //
      html`<div>${this.pageData.videos.map((v) => this.renderVideoListing(v))}</div>`,
    ];
    const dirTitle = this.pageData.dirLabel ? html`<h2>${this.pageData.dirLabel}</h2>` : html``;
    return html`
      ${dirTitle}
      <div class="listing">${listings}</div>

      <div id="scrim" @click=${this.gotoDirListingPage}></div>
      <page-player manifest=""></page-player>
    `;
  }

  escapeALittle(fileUri: string): string {
    return fileUri.replace("#", encodeURIComponent("#"));
  }

  async gotoVideoPlayerPage(ev: CustomEvent) {
    const player = await this.pagePlayer;
    this.goto(PATH_PREFIX + "" + ev.detail.webRelPath);
  }

  gotoDirListingPage() {
    this.goto(PATH_PREFIX + "" + this.pageData?.webDirRelPath);
  }

  getScrim(): HTMLElement {
    return this.shadowRoot!.querySelector("#scrim")!;
  }

  async startPlayer(p: AutoplayVideoFile) {
    const player = await this.pagePlayer;
    player.manifest = PATH_PREFIX + "/files" + this.escapeALittle(p.webDataPath) + "?sig=" + encodeURIComponent(p.sig);
    const sv = player.shadowRoot!.querySelector("shaka-video")! as ShakaVideoElement;

    sv.src = player.manifest;
    sv.autoplay = true;
    player.size = "big";
    this.getScrim().style.display = "block";
  }

  goto(url: string) {
    routerShared.goto(url);
    window.history.pushState({}, "", url);
  }

  async closePlayer() {
    const player = await this.pagePlayer;
    if (player.size === "hidden") {
      return;
    }
    player.size = "hidden";
    this.getScrim().style.display = "none";
  }
}