用koa2 和 html javascript做了一个视频列表功能

服务器部分

bash 复制代码
{
  "name": "koa-vite-video",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node app.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@koa/cors": "^5.0.0",
    "ffmpeg": "^0.0.4",
    "fluent-ffmpeg": "^2.1.3",
    "koa": "^2.15.3",
    "koa-bodyparser": "^4.4.1",
    "koa-router": "^12.0.1",
    "koa-static": "^5.0.0",
    "mysql": "^2.18.1",
    "nodemon": "^3.1.4"
  },
  "devDependencies": {
    "vite": "^5.3.5"
  }
}

其中nodejs中使用 fluent-ffmpeg 要和ffmpeg软件一起使用。

主要功能就是在网页中能够看到视频图片的缩略图。

下载地址 https://ffmpeg.org/

bash 复制代码
const Koa = require("koa");
const Router = require("koa-router");
const cors = require("@koa/cors");
const fs = require("fs");
const path = require("path");
const ffmpeg = require("fluent-ffmpeg");

// Set paths to ffmpeg and ffprobe executables
const ffmpegPath =
  "D:\\Users\\lhl\\Desktop\\ffmpeg-master-latest-win64-gpl\\bin\\ffmpeg.exe";
const ffprobePath =
  "D:\\Users\\lhl\\Desktop\\ffmpeg-master-latest-win64-gpl\\bin\\ffprobe.exe";

ffmpeg.setFfmpegPath(ffmpegPath);
ffmpeg.setFfprobePath(ffprobePath);

const app = new Koa();
const router = new Router();

app.use(cors());

const staticPath = path.join(__dirname, "public");

router.get("/files", async (ctx) => {
  const files = fs.readdirSync(staticPath).map((file) => ({ name: file }));
  ctx.body = files;
});

router.get("/thumbnail/:fileName", async (ctx) => {
  const fileName = ctx.params.fileName;
  const videoPath = path.resolve(staticPath, fileName);
  const thumbnailPath = path.resolve(staticPath, `${fileName}.png`);

  if (!fs.existsSync(videoPath)) {
    ctx.status = 404;
    ctx.body = "Video not found";
    return;
  }

  // If thumbnail doesn't exist, generate it
  if (!fs.existsSync(thumbnailPath)) {
    try {
      await new Promise((resolve, reject) => {
        ffmpeg(videoPath)
          .on("end", resolve)
          .on("error", (err) => {
            console.error(
              `Error generating thumbnail for ${fileName}: ${err.message}`
            );
            reject(err);
          })
          .screenshots({
            timestamps: [1], // Capture at 1 second instead of 0
            filename: `${fileName}.png`,
            folder: staticPath,
            size: "200x150",
          });
      });
    } catch (err) {
      ctx.status = 500;
      ctx.body = "Error generating thumbnail";
      return;
    }
  }

  ctx.type = "image/png";
  ctx.body = fs.createReadStream(thumbnailPath);
});

router.get("/video/:fileName", async (ctx) => {
  const range = ctx.headers.range;
  const fileName = ctx.params.fileName;

  if (!range) {
    ctx.status = 400;
    ctx.body = "Requires Range header";
    return;
  }

  const videoPath = path.resolve(staticPath, fileName);
  if (!fs.existsSync(videoPath)) {
    ctx.status = 404;
    ctx.body = "Video not found";
    return;
  }

  const videoSize = fs.statSync(videoPath).size;
  const CHUNK_SIZE = 1024 * 1024;

  const startMatch = range.match(/bytes=(\d+)-/);
  const start = startMatch ? Number(startMatch[1]) : 0;

  if (start >= videoSize) {
    ctx.status = 416;
    ctx.set("Content-Range", `bytes */${videoSize}`);
    return;
  }

  const end = Math.min(start + CHUNK_SIZE - 1, videoSize - 1);
  const contentLength = end - start + 1;

  const headers = {
    "Content-Range": `bytes ${start}-${end}/${videoSize}`,
    "Accept-Ranges": "bytes",
    "Content-Length": contentLength,
    "Content-Type": "video/mp4",
  };

  ctx.set(headers);
  ctx.status = 206;

  const videoStream = fs.createReadStream(videoPath, { start, end });
  ctx.body = videoStream;
});

app.use(router.routes());
app.use(router.allowedMethods());

app.listen(3000, () => {
  console.log("Server is running on http://localhost:3000");
});

前端部分

bash 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Video Stream</title>
  <style>
    body {
      display: flex;
      justify-content: center;
      align-items: center;
      flex-direction: column;
      height: 100vh;
      margin: 0;
    }

    .grid-container {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
      gap: 10px;
      padding: 10px;
      width: 100%;
      max-width: 1200px;
      overflow: auto;
    }

    .grid-item {
      display: flex;
      justify-content: center;
      align-items: center;
      border: 1px solid #ddd;
      padding: 10px;
      cursor: pointer;
    }

    .grid-item video {
      width: 100%;
      height: auto;
    }

    .full-screen-video {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0, 0, 0, 0.8);
      display: flex;
      justify-content: center;
      align-items: center;
      z-index: 1000;
    }

    .full-screen-video video {
      width: 80%;
      height: auto;
    }

    .full-screen-video .close-btn {
      position: absolute;
      top: 10px;
      right: 10px;
      background: rgba(255, 255, 255, 0.7);
      border: none;
      border-radius: 5px;
      padding: 5px 10px;
      cursor: pointer;
      font-size: 16px;
    }
  </style>
</head>
<body>
  <div class="grid-container" id="fileList"></div>

  <script>
    const fileList = document.getElementById("fileList");
 
    // 获取文件列表并展示
    function loadFiles() {
      fetch("http://localhost:3000/files")
        .then((response) => response.json())
        .then((files) => {
          fileList.innerHTML = "";
          files.forEach((file) => {
            const div = document.createElement("div");
            div.className = "grid-item";
 
            const video = document.createElement("video");
            video.dataset.src = `http://localhost:3000/video/${file.name}`; // 使用 data-src 来存储真实的视频 URL
            video.controls = true;
            video.muted = true;
            video.width = 300; // 控制缩略图大小
 
            // IntersectionObserver 用于懒加载
            const observer = new IntersectionObserver((entries) => {
              entries.forEach((entry) => {
                if (entry.isIntersecting) {
                  video.src = video.dataset.src; // 进入视口时加载视频
                  observer.unobserve(entry.target); // 停止观察
                }
              });
            }, {
              rootMargin: '0px',
              threshold: 0.1
            });
 
            observer.observe(video); // 开始观察视频元素
 
            div.appendChild(video);
 
            div.addEventListener("click", () => openFullScreenVideo(file.name));
            fileList.appendChild(div);
          });
        })
        .catch((error) => console.error("Error loading files:", error));
    }
 
    // 打开全屏视频播放
    function openFullScreenVideo(fileName) {
      const fullScreenContainer = document.createElement("div");
      fullScreenContainer.className = "full-screen-video";
 
      const video = document.createElement("video");
      video.src = `http://localhost:3000/video/${fileName}`;
      video.controls = true;
      video.autoplay = true;
      fullScreenContainer.appendChild(video);
 
      const closeBtn = document.createElement("button");
      closeBtn.textContent = "Close";
      closeBtn.className = "close-btn";
      closeBtn.addEventListener("click", () => {
        document.body.removeChild(fullScreenContainer);
      });
      fullScreenContainer.appendChild(closeBtn);
 
      document.body.appendChild(fullScreenContainer);
    }
 
    // 初始化文件列表
    loadFiles();
  </script>
</body>
</html>
相关推荐
codeejun44 分钟前
每日一Go·MySQL-11、覆盖索引 & 回表原理
javascript·数据库·mysql
用户298698530141 小时前
React 前端将 Excel 工作表导出为图片的技术实践
javascript·react.js·excel
小豆庄2 小时前
测试视频里的人脸车牌敏感信息自动化脱敏方案
计算机视觉·目标跟踪·ffmpeg·音视频·边缘计算·视频·敏捷流程
光泽雨3 小时前
WPF 音视频四大核心方案对比
wpf·音视频
揽秀亭长4 小时前
音频转乐谱如何实现?四类AI 扒谱工具的流程
音视频
麦麦麦当劳大王12 小时前
【油猴脚本】Boss直聘 岗位职责复制
开发语言·前端·javascript
只睡四小时13 小时前
Three.js Sub-tick:漏判 60.7%→0
开发语言·javascript·性能优化·ecmascript·游戏开发·three.js·碰撞检测
excel14 小时前
基于 GPT 的自动发布文章接口方案:从一句话或一张图片生成并发布内容
前端·javascript·后端
敢敢のwings19 小时前
SimpleMemVLA 深度解读:把原生视频上下文变成机器人的记忆
数码相机·机器人·音视频
OpsEye20 小时前
大模型 Token 计量不准,是什么原因,怎么解决??
javascript·ai编程