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=[