用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>
相关推荐
华清远见IT开放实验室2 小时前
【项目案例】物联网比较好的10+练手项目推荐,附项目文档/源码/视频
物联网·音视频
北岛寒沫3 小时前
JavaScript(JS)学习笔记 1(简单介绍 注释和输入输出语句 变量 数据类型 运算符 流程控制 数组)
javascript·笔记·学习
everyStudy3 小时前
JavaScript如何判断输入的是空格
开发语言·javascript·ecmascript
无心使然云中漫步4 小时前
GIS OGC之WMTS地图服务,通过Capabilities XML描述文档,获取matrixIds,origin,计算resolutions
前端·javascript
Bug缔造者5 小时前
Element-ui el-table 全局表格排序
前端·javascript·vue.js
xnian_5 小时前
解决ruoyi-vue-pro-master框架引入报错,启动报错问题
前端·javascript·vue.js
麒麟而非淇淋6 小时前
AJAX 入门 day1
前端·javascript·ajax
2401_858120536 小时前
深入理解MATLAB中的事件处理机制
前端·javascript·matlab
阿树梢6 小时前
【Vue】VueRouter路由
前端·javascript·vue.js
随笔写7 小时前
vue使用关于speak-tss插件的详细介绍
前端·javascript·vue.js