PHP 视频播放平台搭建实践


本来是想练手golang的,但一看服务器已经是PHP的形状了,懒癌发作了,PHP不也挺好吗(

PHP视频播放平台搭建实践

项目预览链接

http://mxzfun.com/videoAll/public/index.php

完整项目程序

见如下Github链接:

https://github.com/yosoroQ/videoAll/tree/main

项目展示

PC端

移动端

项目引言

项目背景

  • mxzfun.com是我的日常博客,有些文章我会放些视频上去用以记录,但他们都是以单体的形式出现,你要去“刷”视频,实属不方便。
  • 所以想出构建一个独立页面用于读取和播放服务器里所上传的视频。

技术选型

  • PHP + 模板渲染 + 原生 JS + CSS Grid
  • 没啥好说的,其实就是WordPress他用的是PHP,我也懒得再去服务器里新建个其他程序框架的项目了。

技术架构概览

项目目录结构

videoALL/
│
├─ public/           # 网站公开访问目录
│   ├─ index.php     # 入口文件,路由和模板调用
│   └─ assets/       # 静态资源:CSS、JS
        ├─ script.js
        └─ style.css
│
├─ app/              # 核心应用逻辑
│   ├─ Video.php     # 视频扫描、过滤、排序、分页
│   └─ Category.php  # 分类目录扫描、按钮生成
│
├─ templates/        # HTML 模板
│   ├─ header.php    # 公共头部
│   ├─ footer.php    # 公共底部
│   └─ video_grid.php# 视频网格渲染
│
├─ config/           # 配置文件
│   └─ config.php    # 上传目录、排除目录、分页数量、支持格式
│
└─ logs/             # 可选日志目录

核心组件

视频扫描模块(Video.php

  • 扫描 uploads/ 目录及子目录
  • 过滤视频格式(mp4、webm、mov 等)
  • 处理中文和空格文件名(URL 编码)
  • 根据修改时间排序,实现最新视频优先显示
  • 分页切片,支持每页显示固定数量(如 16 个)

分类模块(Category.php

  • 读取 uploads/ 下一级子目录
  • 排除特定目录(如 Aspose.Wordsppress_uploadssea
  • 自动生成分类按钮,点击切换显示对应分类视频

分页与网格渲染(video_grid.php + style.css + script.js

  • 网格渲染视频块,显示视频标题
  • CSS grid + aspect-ratio 保证比例统一
  • JS 实现悬停播放,提升交互体验
  • 分页链接展示当前页码和总页数,支持快速跳转

项目目录设计

public/ —— 网站入口与静态资源

  • 用途

    • 公开访问目录,浏览器访问的根路径
    • 放置入口文件 index.php
    • 静态资源文件:CSS、JS、图片等
    • 视频上传目录 uploads/,存放 WordPress 的视频文件
  • 设计理念

    • 保证前端和用户可直接访问的文件集中管理
    • 将核心业务逻辑和模板分离,避免用户直接访问敏感 PHP 逻辑
  • 具体结构

    public/
    ├─ index.php       # 网站入口
    ├─ uploads/        # 视频文件存放
    │   ├─ 2020/
    │   ├─ 2021/
    │   └─ ...
    └─ assets/         # 静态资源
        ├─ style.css
        └─ script.js

app/ —— 核心业务逻辑

  • 用途

    • 处理网站的核心功能,包括视频扫描、分类生成、分页计算等
  • 核心文件

    • Video.php

      • 扫描指定目录及子目录的视频文件
      • 过滤支持格式(mp4、mov、webm 等)
      • 对中文或空格文件名进行 URL 编码处理
      • 根据修改时间排序,实现最新视频优先显示
      • 切片分页数据供模板渲染
    • Category.php

      • 读取一级子目录生成分类列表
      • 排除特定目录(如 Aspose.Wordsppress_uploadssea
      • 输出分类按钮 HTML 或数组供模板调用
  • 设计理念

    • 模块化:核心逻辑与模板分离,便于单元测试
    • 可扩展性:未来可加入缩略图生成、视频搜索、标签等功能
    • 可维护性:任何逻辑修改只需在 app/ 内处理,无需改动模板

templates/ —— HTML 模板

  • 用途

    • 渲染网页结构,前端布局与后端逻辑分离
    • 提供统一的头部、尾部和视频网格模板
  • 核心文件

    • header.php:包含 meta、CSS 引入、导航栏等
    • footer.php:包含 JS 引入,闭合标签
    • video_grid.php:循环渲染视频块、标题、分页和分类按钮
  • 设计理念

    • 可复用:头尾公共部分只需修改一次
    • 分层清晰:模板只负责渲染数据,不处理业务逻辑
    • 可扩展:可轻松增加广告位、搜索框或筛选组件

config/ —— 可配置参数

  • 用途

    • 集中管理项目可变参数,避免在代码中硬编码
  • 核心文件

    • config.php

      • 上传目录路径
      • 排除目录列表
      • 每页显示的视频数量
      • 支持的视频格式列表
  • 设计理念

    • 集中配置:修改参数无需触碰核心逻辑或模板
    • 灵活性:方便在不同服务器环境或不同需求下快速调整

核心功能实现

  • Video.php:扫描视频、过滤格式、按时间排序、分页切片
  • Category.php:自动生成分类按钮、排除无关目录
  • index.php + video_grid.php:渲染分类按钮、分页与网格布局
  • CSS & JS:保证统一比例、响应式布局、悬停自动播放

视频扫描与过滤(Video.php

<?php
class Video {
    private $uploadDir;
    private $allowedFormats = ['mp4','webm','mov'];

    public function __construct($uploadDir) {
        $this->uploadDir = $uploadDir;
    }

    // 扫描目录及子目录获取视频列表
    public function getVideos($category = null) {
        $videos = [];
        $dir = $category ? $this->uploadDir . '/' . $category : $this->uploadDir;
        $videos = $this->scanDir($dir);
        // 按修改时间倒序
        usort($videos, function($a,$b){ return filemtime($b) - filemtime($a); });
        return $videos;
    }

    private function scanDir($dir) {
        $files = [];
        if(!is_dir($dir)) return [];
        foreach(scandir($dir) as $item){
            if($item === '.' || $item === '..') continue;
            $path = $dir . '/' . $item;
            if(is_dir($path)){
                $files = array_merge($files, $this->scanDir($path));
            } else {
                $ext = strtolower(pathinfo($item, PATHINFO_EXTENSION));
                if(in_array($ext, $this->allowedFormats)){
                    $files[] = $path;
                }
            }
        }
        return $files;
    }
}

分类按钮生成(Category.php

<?php
class Category {
    private $uploadDir;
    private $excludeDirs = ['Aspose.Words','ppress_uploads','sea'];

    public function __construct($uploadDir){
        $this->uploadDir = $uploadDir;
    }

    public function getCategories(){
        $categories = [];
        foreach(scandir($this->uploadDir) as $item){
            if($item === '.' || $item === '..') continue;
            $path = $this->uploadDir.'/'.$item;
            if(is_dir($path) && !in_array($item, $this->excludeDirs)){
                $categories[] = $item;
            }
        }
        return $categories;
    }
}

分页逻辑(index.php

<?php
    require_once '../app/Video.php';
    require_once '../app/Category.php';
    require_once '../config/config.php';

    $videoObj = new Video($uploadDir);
    $categoryObj = new Category($uploadDir);

    $category = $_GET['category'] ?? null;
    $page = max(1, intval($_GET['page'] ?? 1));
    $perPage = $perPage ?? 16;

    $videos = $videoObj->getVideos($category);
    $totalPages = ceil(count($videos)/$perPage);
    $videosPage = array_slice($videos, ($page-1)*$perPage, $perPage);
    $categories = $categoryObj->getCategories();
?>

网格布局渲染(video_grid.php + style.css

video_grid.php

<h2>视频展示</h2>

<!-- 分类按钮 -->
<div class="categories">
<?php foreach($categories as $cat): ?>
    <a href="?category=<?=urlencode($cat)?>" class="category-btn"><?=$cat?></a>
<?php endforeach; ?>
</div>

<!-- 视频网格 -->
<div class="video-grid">
<?php foreach($videosPage as $video): ?>
    <div class="video-item">
        <video controls muted>
            <source src="<?=str_replace('../public','',$video)?>" type="video/mp4">
        </video>
    </div>
<?php endforeach; ?>
</div>

<!-- 分页 -->
<div class="pagination">
<?php for($i=1;$i<=$totalPages;$i++): ?>
    <a href="?category=<?=urlencode($category)?>&page=<?=$i?>" class="<?=($i==$page)?'active':''?>"><?=$i?></a>
<?php endfor; ?>
</div>

style.css

.categories { margin-bottom: 15px; }
.category-btn { margin-right: 5px; padding:5px 10px; background:#eee; text-decoration:none; border-radius:4px; }
.category-btn:hover { background:#ccc; }

.video-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 10px;
}

.video-item video {
    width: 100%;
    aspect-ratio: 16/9;
    object-fit: cover;
    display: block;
}

.pagination a {
    margin: 0 5px;
    padding: 5px 10px;
    background:#eee;
    text-decoration:none;
    border-radius:4px;
}
.pagination a.active { background:#999; color:#fff; }

悬停自动播放(script.js

document.addEventListener('DOMContentLoaded', function(){
    document.querySelectorAll('.video-item video').forEach(function(video){
        video.addEventListener('mouseenter', function(){ video.play(); });
        video.addEventListener('mouseleave', function(){ video.pause(); });
    });
});

技术难点与解决方案

中文/空格文件名 URL 编码处理

难点

  • WordPress 上传目录中视频文件可能包含中文、空格或特殊字符
  • 直接在 HTML <video> 标签中引用会导致浏览器无法识别路径,出现 404 错误或无法播放

解决方案

  • 在 PHP 后端处理视频路径时,使用 rawurlencode() 对文件名进行编码
  • 例如:

    $videoUrl = str_replace('../public','', $video);
    $videoUrl = implode('/', array_map('rawurlencode', explode('/', $videoUrl)));
  • 保证生成的 URL 对浏览器友好,同时保留原有文件名信息

设计理念

  • 兼容性:无论中文、空格还是特殊字符都能正常播放
  • 通用性:未来新增视频文件无需手动处理

竖屏视频比例统一显示

难点

  • 视频可能存在横屏(16:9)、竖屏(9:16)等不同比例
  • 竖屏视频高度过高会破坏网格布局,影响整体视觉体验

解决方案

  • 使用 CSS aspect-ratio 或通过 object-fit: cover 强制统一显示比例
  • 代码示例:

    .video-item video {
        width: 100%;
        aspect-ratio: 16/9;
        object-fit: cover;
    }
  • 网格容器保持固定比例,视频内容自动裁剪,竖屏和横屏视频显示效果一致

设计理念

  • 视觉统一:保证网格整齐,不受视频原始尺寸影响
  • 用户体验:每个视频块大小一致,页面浏览舒适

移动端适配与响应式布局

难点

  • 用户在手机、平板等不同屏幕上访问网站
  • 网格列数、间距、按钮布局需要自适应

解决方案

  • 使用 CSS Grid 布局 + auto-fillminmax() 控制列数
  • 媒体查询调整移动端样式:

    @media (max-width: 768px) {
        .video-grid {
            grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
        }
    }
  • 按钮、分页等元素也使用弹性布局或百分比宽度

设计理念

  • 响应式:无需单独开发移动端页面
  • 体验一致:PC、平板、手机浏览视觉效果统一

分页和分类组合查询逻辑

难点

  • 用户可以选择分类并分页浏览视频
  • 分类切换或分页时需要正确更新显示视频
  • 需要避免数组越界或分页错误

解决方案

  • 后端逻辑统一:

    1. 根据分类过滤视频列表
    2. 按修改时间倒序排列
    3. 切片数组实现分页
    $videosPage = array_slice($videos, ($page-1)*$perPage, $perPage);
  • 前端分页按钮根据总页数和当前页高亮显示
  • 分类按钮点击时刷新页面或传递 GET 参数,保证分页逻辑正确

设计理念

  • 清晰逻辑:分页和分类相互独立又互相配合

声明:三二一的一的二|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - PHP 视频播放平台搭建实践


三二一的一的二