用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>
相关推荐
学不会•1 小时前
css数据不固定情况下,循环加不同背景颜色
前端·javascript·html
EasyNTS2 小时前
H.264/H.265播放器EasyPlayer.js视频流媒体播放器关于websocket1006的异常断连
javascript·h.265·h.264
安静读书2 小时前
Python解析视频FPS(帧率)、分辨率信息
python·opencv·音视频
佑华硬盘拷贝机3 小时前
音频档案批量拷贝:专业SD拷贝机解决方案
音视频
EasyNVR3 小时前
NVR管理平台EasyNVR多个NVR同时管理:全方位安防监控视频融合云平台方案
安全·音视频·监控·视频监控
活宝小娜3 小时前
vue不刷新浏览器更新页面的方法
前端·javascript·vue.js
程序视点4 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
coldriversnow4 小时前
在Vue中,vue document.onkeydown 无效
前端·javascript·vue.js
我开心就好o4 小时前
uniapp点左上角返回键, 重复来回跳转的问题 解决方案
前端·javascript·uni-app
刚刚好ā4 小时前
js作用域超全介绍--全局作用域、局部作用、块级作用域
前端·javascript·vue.js·vue