Mercurial > code > home > repos > video
changeset 6:ccfea3625cf6
render thumbs and display them (no video player at all atm)
author | drewp@bigasterisk.com |
---|---|
date | Sun, 09 Apr 2023 18:02:25 -0700 |
parents | 75b54be050bc |
children | de93b9133acb |
files | Dockerfile src/VideoPage.ts src/VideoSection.ts video.py |
diffstat | 4 files changed, 71 insertions(+), 8 deletions(-) [+] |
line wrap: on
line diff
--- a/Dockerfile Thu Mar 30 20:39:40 2023 -0700 +++ b/Dockerfile Sun Apr 09 18:02:25 2023 -0700 @@ -17,7 +17,7 @@ RUN pnpm set registry "https://bigasterisk.com/js/" WORKDIR /opt -RUN apt update && apt-get install -y git +RUN apt update && apt-get install -y git ffmpegthumbnailer COPY pyproject.toml pdm.lock ./ RUN pdm sync
--- a/src/VideoPage.ts Thu Mar 30 20:39:40 2023 -0700 +++ b/src/VideoPage.ts Sun Apr 09 18:02:25 2023 -0700 @@ -5,6 +5,7 @@ interface VideoFile { webRelPath: string; label: string; + thumbRelPath: string; } interface Subdir { label: string; @@ -53,6 +54,8 @@ a { color: white; font-size: 20px; + text-transform: uppercase; + text-underline-offset: 10px; } #path-segs > span { color: white; @@ -86,7 +89,7 @@ <div id="path-segs">${this.pathSegs.map((seg) => html`<span><a href="./?${subdirQuery(seg.subdir)}">${seg.label}</a></span>`)}</div> ${this.subdirs.map((s) => html`<div class="subdir"><a href="${"./?" + subdirQuery(s.path)}">${s.label}</a></div>`)} - ${this.videos.map((v) => html`<video-section title="${v.label}" manifest=${v.webRelPath}></video-section>`)} + ${this.videos.map((v) => html`<video-section thumbRelPath="${v.thumbRelPath}" title="${v.label}" manifest=${v.webRelPath}></video-section>`)} `; } }
--- a/src/VideoSection.ts Thu Mar 30 20:39:40 2023 -0700 +++ b/src/VideoSection.ts Sun Apr 09 18:02:25 2023 -0700 @@ -5,6 +5,7 @@ @customElement("video-section") export class VideoSection extends LitElement { @property({ type: String }) manifest: string | undefined; + @property({ type: String }) thumbRelPath: string | undefined; @property({ type: String }) title: string = "(unknown)"; @property({ type: String }) big: boolean = false; @@ -37,11 +38,11 @@ const tx = (document.body.clientWidth - inw * scl) / 2, ty = (document.body.clientHeight - inh * scl) / 2; const style = this.big ? `transform: translate(${-inx - inw / 2}px,${-iny - inh / 2}px) scale(${outh / inh}) translate(${tx}px,${ty}px);` : ""; - console.log(document.body.clientWidth); return html` <section> <h1>${this.title}</h1> - <shaka-video class="${vidCls}" style="${style}" id="video" width="720" height="480" src="${this.manifest}" controls></video> + <!-- <shaka-video class="${vidCls}" style="${style}" id="video" width="720" height="480" src="${this.manifest}" controls></video>--> + <img src="${this.thumbRelPath}" /> </section> `; }
--- a/video.py Thu Mar 30 20:39:40 2023 -0700 +++ b/video.py Sun Apr 09 18:02:25 2023 -0700 @@ -1,4 +1,10 @@ +import asyncio +from dataclasses import dataclass +from pathlib import Path +import hashlib import logging +import re +from typing import Iterable from prometheus_client import Gauge from starlette.applications import Starlette @@ -7,30 +13,83 @@ from starlette.routing import Route from starlette_exporter import PrometheusMiddleware, handle_metrics -from video_service import findInDir, findSubdirs +from video_service import VideoFile logging.basicConfig(level=logging.DEBUG) log = logging.getLogger() +def vf(p: Path, label: str): + return VideoFile(p, './files/' + str(p.relative_to('/data')), label) + +def thumbWebPath(rel: str)->str: + return './files/' + rel + +@dataclass +class VideoFileStore: + top: Path + + def findInDir(self, subdir: str) -> Iterable[VideoFile]: + if subdir[0] != '/': raise ValueError + here = self.top / subdir[1:] + manifests = list(here.glob('*.mpd')) + if manifests: + p = manifests[0] + label = p.parent.name + yield vf(p, label) + return + for p in sorted(list(here.glob('*.mp4')) + list(here.glob('*.webm'))): + label = re.sub(r' \[[^\]]+\]\.\w+', '', p.name) + yield vf(p, label) + + + def findSubdirs(self, subdir: str) -> Iterable: + if subdir[0] != '/': raise ValueError + here = self.top / subdir[1:] + for p in here.iterdir(): + if p.is_dir() and p.name not in {'_thumb'}: + yield {'label': p.name, 'path': '/' + str(p.relative_to(self.top))} + + def thumbPath(self, vf: VideoFile) -> str: + sha256 = hashlib.sha256() + with open(vf.diskPath, 'rb') as f: + firstMb = f.read(1<<20) + sha256.update(firstMb) + cksum = sha256.hexdigest() + return f'_thumb/{cksum}.jpg' + + async def getOrCreateThumb(self, vf: VideoFile) -> str: + p = self.top / self.thumbPath(vf) + if not p.exists(): + sp = asyncio.create_subprocess_exec('ffmpegthumbnailer', + '-s', '250', + '-i', str(vf.diskPath), + '-o', str(p)) + await sp + return thumbWebPath(str(p.relative_to(self.top))) + def root(req): return HTMLResponse("api") -def videos(req: Request) -> JSONResponse: +async def videos(req: Request) -> JSONResponse: subdir = req.query_params.get('subdir', '/') # danger user input + vfInDir = store.findInDir(subdir) return JSONResponse({ "videos": [{ 'webRelPath': vf.webRelPath, 'label': vf.label, - } for vf in findInDir(subdir)], + 'thumbRelPath': await store.getOrCreateThumb(vf), + } for vf in vfInDir], "subdirs": - list(findSubdirs(subdir)), + list(store.findSubdirs(subdir)), }) +store = VideoFileStore(top=Path('/data')) def main(): + app = Starlette( debug=True, routes=[