文章目录[隐藏]
详细教程:为WordPress网站打造内嵌在线简易视频编辑与短片制作工具
引言:为什么网站需要内置视频编辑功能?
在当今数字内容爆炸的时代,视频已成为最受欢迎的内容形式之一。据统计,超过85%的互联网用户每周都会观看在线视频内容。对于内容创作者、营销人员和网站所有者来说,能够快速制作和编辑视频已成为一项核心竞争力。
然而,传统的视频编辑流程往往复杂且耗时:用户需要下载专业软件、学习复杂操作、导出文件后再上传到网站。这一过程不仅效率低下,还可能导致用户流失。通过在WordPress网站中内置简易视频编辑工具,我们可以:
- 大幅降低用户制作视频的门槛
- 提高用户参与度和内容产出率
- 创造独特的用户体验和竞争优势
- 减少对外部服务的依赖,保护用户数据隐私
本教程将详细指导您如何通过WordPress代码二次开发,为网站添加一个功能完整的在线简易视频编辑与短片制作工具。
第一部分:项目规划与技术选型
1.1 功能需求分析
在开始开发前,我们需要明确工具应具备的核心功能:
基础编辑功能:
- 视频裁剪与分割
- 多视频片段拼接
- 添加背景音乐和音效
- 文本叠加与字幕添加
- 基本滤镜和色彩调整
高级功能(可选):
- 绿幕抠像(色度键控)
- 转场效果
- 动画元素添加
- 语音转字幕
- 模板化快速制作
输出选项:
- 多种分辨率支持(480p、720p、1080p)
- 多种格式输出(MP4、WebM、GIF)
- 直接发布到网站媒体库
1.2 技术架构设计
我们将采用前后端分离的架构:
前端技术栈:
- HTML5 Video API:处理视频播放和基础操作
- Canvas API:实现视频帧处理和滤镜效果
- Web Audio API:处理音频混合
- FFmpeg.wasm:在浏览器中实现视频转码和合成
- React/Vue.js(可选):构建交互式UI
后端技术栈:
- WordPress REST API:处理用户认证和数据存储
- PHP GD库/ImageMagick:服务器端图像处理
- 自定义数据库表:存储用户项目和编辑历史
关键技术挑战与解决方案:
- 浏览器性能限制:采用分段处理和Web Worker
- 大文件处理:使用流式处理和分块上传
- 跨浏览器兼容性:功能检测和渐进增强策略
第二部分:开发环境搭建与基础配置
2.1 创建WordPress插件框架
首先,我们需要创建一个基础的WordPress插件:
<?php
/**
* Plugin Name: 简易在线视频编辑器
* Plugin URI: https://yourwebsite.com/
* Description: 为WordPress网站添加内置的在线视频编辑功能
* Version: 1.0.0
* Author: 您的名称
* License: GPL v2 or later
*/
// 防止直接访问
if (!defined('ABSPATH')) {
exit;
}
// 定义插件常量
define('VIDEO_EDITOR_VERSION', '1.0.0');
define('VIDEO_EDITOR_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('VIDEO_EDITOR_PLUGIN_URL', plugin_dir_url(__FILE__));
// 初始化插件
class Video_Editor_Plugin {
private static $instance = null;
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$this->init_hooks();
}
private function init_hooks() {
// 注册激活和停用钩子
register_activation_hook(__FILE__, array($this, 'activate'));
register_deactivation_hook(__FILE__, array($this, 'deactivate'));
// 初始化
add_action('init', array($this, 'init'));
// 管理菜单
add_action('admin_menu', array($this, 'add_admin_menu'));
// 前端资源
add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_assets'));
// 短代码
add_shortcode('video_editor', array($this, 'video_editor_shortcode'));
}
public function activate() {
// 创建必要的数据库表
$this->create_database_tables();
// 设置默认选项
update_option('video_editor_max_upload_size', 500); // MB
update_option('video_editor_allowed_formats', 'mp4,webm,mov,avi');
update_option('video_editor_default_quality', '720p');
}
public function deactivate() {
// 清理临时文件
$this->cleanup_temp_files();
}
public function init() {
// 注册自定义文章类型(如果需要)
// 初始化REST API端点
add_action('rest_api_init', array($this, 'register_rest_routes'));
}
// 其他方法将在后续部分实现
}
// 启动插件
Video_Editor_Plugin::get_instance();
?>
2.2 创建必要的数据库表
我们需要创建表来存储用户的项目数据:
private function create_database_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$table_name = $wpdb->prefix . 'video_editor_projects';
$sql = "CREATE TABLE IF NOT EXISTS $table_name (
id mediumint(9) NOT NULL AUTO_INCREMENT,
user_id bigint(20) NOT NULL,
project_name varchar(255) NOT NULL,
project_data longtext NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
status varchar(20) DEFAULT 'draft',
PRIMARY KEY (id),
KEY user_id (user_id),
KEY status (status)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
// 创建临时文件记录表
$temp_table = $wpdb->prefix . 'video_editor_temp_files';
$sql_temp = "CREATE TABLE IF NOT EXISTS $temp_table (
id mediumint(9) NOT NULL AUTO_INCREMENT,
file_hash varchar(64) NOT NULL,
file_path varchar(500) NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
expires_at datetime NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY file_hash (file_hash),
KEY expires_at (expires_at)
) $charset_collate;";
dbDelta($sql_temp);
}
第三部分:前端编辑器界面开发
3.1 构建编辑器HTML结构
创建编辑器的主要界面结构:
<!-- 在插件目录中创建templates/editor-frontend.php -->
<div id="video-editor-app" class="video-editor-container">
<!-- 顶部工具栏 -->
<div class="editor-toolbar">
<div class="toolbar-left">
<button id="btn-new-project" class="editor-btn">
<i class="icon-new"></i> 新建项目
</button>
<button id="btn-save-project" class="editor-btn">
<i class="icon-save"></i> 保存项目
</button>
<button id="btn-export" class="editor-btn btn-primary">
<i class="icon-export"></i> 导出视频
</button>
</div>
<div class="toolbar-right">
<div class="project-name">
<input type="text" id="project-name" placeholder="项目名称" value="未命名项目">
</div>
</div>
</div>
<!-- 主工作区 -->
<div class="editor-workspace">
<!-- 左侧资源面板 -->
<div class="panel-left">
<div class="panel-tabs">
<button class="panel-tab active" data-tab="media">媒体库</button>
<button class="panel-tab" data-tab="text">文字</button>
<button class="panel-tab" data-tab="audio">音频</button>
<button class="panel-tab" data-tab="effects">特效</button>
</div>
<div class="panel-content">
<!-- 媒体库内容 -->
<div id="tab-media" class="tab-content active">
<div class="media-actions">
<button id="btn-upload-media" class="action-btn">
<i class="icon-upload"></i> 上传媒体
</button>
<button id="btn-record-video" class="action-btn">
<i class="icon-record"></i> 录制视频
</button>
</div>
<div class="media-library">
<!-- 动态加载媒体项 -->
</div>
</div>
<!-- 其他标签页内容 -->
<!-- ... -->
</div>
</div>
<!-- 中央预览区 -->
<div class="panel-center">
<div class="video-preview-container">
<div class="preview-controls">
<button id="btn-play" class="control-btn">
<i class="icon-play"></i>
</button>
<div class="timeline-container">
<div class="timeline-scrubber"></div>
<div class="timeline-track" id="video-timeline">
<!-- 时间轴轨道 -->
</div>
</div>
<div class="time-display">
<span id="current-time">00:00</span> / <span id="duration">00:00</span>
</div>
</div>
<div class="video-canvas-container">
<canvas id="video-canvas" width="1280" height="720"></canvas>
<video id="source-video" style="display:none;" crossorigin="anonymous"></video>
</div>
</div>
</div>
<!-- 右侧属性面板 -->
<div class="panel-right">
<div class="property-panel">
<h3>视频属性</h3>
<div class="property-group">
<label>裁剪</label>
<div class="crop-controls">
<input type="number" id="crop-start" placeholder="开始时间(秒)" min="0">
<input type="number" id="crop-end" placeholder="结束时间(秒)" min="0">
<button id="btn-apply-crop" class="small-btn">应用</button>
</div>
</div>
<div class="property-group">
<label>音量</label>
<input type="range" id="volume-slider" min="0" max="200" value="100">
<span id="volume-value">100%</span>
</div>
<div class="property-group">
<label>滤镜</label>
<select id="filter-select">
<option value="none">无滤镜</option>
<option value="grayscale">灰度</option>
<option value="sepia">怀旧</option>
<option value="invert">反色</option>
<option value="brightness">亮度增强</option>
</select>
</div>
<div class="property-group">
<label>添加文字</label>
<input type="text" id="text-input" placeholder="输入文字">
<div class="text-controls">
<input type="color" id="text-color" value="#FFFFFF">
<input type="number" id="text-size" min="10" max="100" value="24">
<button id="btn-add-text" class="small-btn">添加</button>
</div>
</div>
</div>
</div>
</div>
<!-- 底部时间轴 -->
<div class="editor-timeline">
<div class="timeline-header">
<div class="track-labels">
<div class="track-label">视频轨道</div>
<div class="track-label">音频轨道</div>
<div class="track-label">文字轨道</div>
</div>
</div>
<div class="timeline-body">
<div class="timeline-tracks">
<!-- 动态生成轨道 -->
</div>
<div class="timeline-ruler">
<!-- 时间刻度 -->
</div>
</div>
</div>
</div>
3.2 实现核心JavaScript编辑器类
创建编辑器的主要JavaScript逻辑:
// 在插件目录中创建assets/js/video-editor-core.js
class VideoEditor {
constructor(config) {
this.config = {
containerId: 'video-editor-app',
maxFileSize: 500 * 1024 * 1024, // 500MB
allowedFormats: ['video/mp4', 'video/webm', 'video/ogg'],
...config
};
this.state = {
currentProject: null,
mediaElements: [],
timelineElements: [],
isPlaying: false,
currentTime: 0,
duration: 0
};
this.init();
}
async init() {
// 初始化DOM元素引用
this.container = document.getElementById(this.config.containerId);
this.canvas = document.getElementById('video-canvas');
this.video = document.getElementById('source-video');
this.ctx = this.canvas.getContext('2d');
// 初始化FFmpeg.wasm
await this.initFFmpeg();
// 绑定事件
this.bindEvents();
// 加载用户媒体库
await this.loadMediaLibrary();
// 初始化时间轴
this.initTimeline();
}
async initFFmpeg() {
// 检查浏览器是否支持WebAssembly
if (!window.WebAssembly) {
console.error('浏览器不支持WebAssembly,部分功能将受限');
return;
}
try {
// 加载FFmpeg.wasm
const { createFFmpeg, fetchFile } = FFmpeg;
this.ffmpeg = createFFmpeg({ log: true });
// 显示加载状态
this.showMessage('正在加载视频处理引擎...', 'info');
await this.ffmpeg.load();
this.showMessage('视频编辑器准备就绪', 'success');
} catch (error) {
console.error('FFmpeg初始化失败:', error);
this.showMessage('视频处理引擎加载失败,基础编辑功能仍可用', 'warning');
}
}
bindEvents() {
// 播放控制
document.getElementById('btn-play').addEventListener('click', () => this.togglePlay());
// 文件上传
document.getElementById('btn-upload-media').addEventListener('click', () => this.openFileUpload());
// 时间轴拖动
this.setupTimelineEvents();
// 属性控制
document.getElementById('volume-slider').addEventListener('input', (e) => {
this.setVolume(e.target.value / 100);
document.getElementById('volume-value').textContent = `${e.target.value}%`;
});
document.getElementById('filter-select').addEventListener('change', (e) => {
this.applyFilter(e.target.value);
});
// 文字添加
document.getElementById('btn-add-text').addEventListener('click', () => {
const text = document.getElementById('text-input').value;
const color = document.getElementById('text-color').value;
const size = document.getElementById('text-size').value;
if (text.trim()) {
this.addTextElement(text, color, parseInt(size));
document.getElementById('text-input').value = '';
}
});
// 裁剪应用
document.getElementById('btn-apply-crop').addEventListener('click', () => {
const start = parseFloat(document.getElementById('crop-start').value) || 0;
const end = parseFloat(document.getElementById('crop-end').value) || this.state.duration;
if (end > start) {
this.cropVideo(start, end);
}
});
}
async openFileUpload() {
// 创建文件输入元素
const input = document.createElement('input');
input.type = 'file';
input.accept = this.config.allowedFormats.join(',');
input.multiple = true;
input.onchange = async (e) => {
const files = Array.from(e.target.files);
for (const file of files) {
// 检查文件大小
if (file.size > this.config.maxFileSize) {
this.showMessage(`文件 ${file.name} 超过大小限制`, 'error');
continue;
}
// 检查文件类型
if (!this.config.allowedFormats.includes(file.type)) {
this.showMessage(`文件 ${file.name} 格式不支持`, 'error');
continue;
}
// 上传文件
await this.uploadMediaFile(file);
}
};
input.click();
}
async uploadMediaFile(file) {
// 创建FormData
const formData = new FormData();
formData.append('action', 'video_editor_upload');
formData.append('file', file);
formData.append('nonce', this.config.nonce);
try {
this.showMessage(`正在上传 ${file.name}...`, 'info');
const response = await fetch(this.config.ajaxUrl, {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
this.showMessage(`${file.name} 上传成功`, 'success');
this.addMediaToLibrary(result.data);
} else {
this.showMessage(`上传失败: ${result.data.message}`, 'error');
}
} catch (error) {
console.error('上传失败:', error);
this.showMessage('上传失败,请检查网络连接', 'error');
}
}
addMediaToLibrary(mediaData) {
// 创建媒体库项目
const mediaItem = {
id: mediaData.id,
type: mediaData.type,
url: mediaData.url,
thumbnail: mediaData.thumbnail || mediaData.url,
duration: mediaData.duration || 0,
name: mediaData.name
};
this.state.mediaElements.push(mediaItem);
// 更新媒体库UI
this.renderMediaLibrary();
// 如果这是第一个视频,自动加载到编辑器
if (mediaData.type.startsWith('video/') && !this.state.currentProject) {
this.loadVideo(mediaItem);
}
}
async loadVideo(mediaItem) {
this.showMessage(`正在加载视频: ${mediaItem.name}`, 'info');
// 设置视频源
this.video.src = mediaItem.url;
// 等待视频元数据加载
await new Promise((resolve) => {
this.video.onloadedmetadata = () => {
this.state.duration = this.video.duration;
this.updateDurationDisplay();
resolve();
};
});
// 初始化项目
this.state.currentProject = {
id: Date.now().toString(),
name: '未命名项目',
sourceVideo: mediaItem,
edits: [],
elements: []
};
// 开始渲染循环
this.startRenderLoop();
this.showMessage('视频加载完成,可以开始编辑', 'success');
}
startRenderLoop() {
const renderFrame = () => {
if (!this.state.isPlaying && this.state.currentTime === this.video.currentTime) {
requestAnimationFrame(renderFrame);
return;
}
this.state.currentTime = this.video.currentTime;
this.updateTimeDisplay();
this.updateTimelinePosition();
// 清除画布
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// 绘制视频帧
this.ctx.drawImage(
this.video,
0, 0, this.video.videoWidth, this.video.videoHeight,
0, 0, this.canvas.width, this.canvas.height
);
// 应用当前滤镜
this.applyCurrentFilter();
// 绘制叠加元素(文字、图形等)
this.renderOverlayElements();
requestAnimationFrame(renderFrame);
};
renderFrame();
}
applyCurrentFilter() {
const filter = document.getElementById('filter-select').value;
switch(filter) {
case 'grayscale':
this.ctx.filter = 'grayscale(100%)';
this.ctx.drawImage(this.canvas, 0, 0);
this.ctx.filter = 'none';
break;
case 'sepia':
this.ctx.filter = 'sepia(100%)';
this.ctx.drawImage(this.canvas, 0, 0);
this.ctx.filter = 'none';
break;
case 'invert':
this.ctx.filter = 'invert(100%)';
this.ctx.drawImage(this.canvas, 0, 0);
this.ctx.filter = 'none';
break;
case 'brightness':
this.ctx.filter = 'brightness(150%)';
this.ctx.drawImage(this.canvas, 0, 0);
this.ctx.filter = 'none';
break;
}
}
addTextElement(text, color, size) {
const textElement = {
id: `text_${Date.now()}`,
type: 'text',
content: text,
color: color,
size: size,
position: { x: 50, y: 50 },
startTime: this.state.currentTime,
duration: 5, // 显示5秒
font: 'Arial'
};
this.state.currentProject.elements.push(textElement);
this.addToTimeline(textElement);
}
renderOverlayElements() {
const currentTime = this.state.currentTime;
this.state.currentProject.elements.forEach(element => {
if (currentTime >= element.startTime &&
currentTime <= element.startTime + element.duration) {
if (element.type === 'text') {
this.ctx.fillStyle = element.color;
this.ctx.font = `${element.size}px ${element.font}`;
this.ctx.fillText(element.content, element.position.x, element.position.y);
}
}
});
}
async cropVideo(startTime, endTime) {
if (!this.ffmpeg) {
this.showMessage('视频裁剪需要FFmpeg支持,请稍后再试', 'warning');
return;
}
this.showMessage('正在裁剪视频...', 'info');
try {
// 获取视频文件
const response = await fetch(this.state.currentProject.sourceVideo.url);
const videoBlob = await response.blob();
// 写入FFmpeg文件系统
this.ffmpeg.FS('writeFile', 'input.mp4', await fetchFile(videoBlob));
// 执行裁剪命令
await this.ffmpeg.run(
'-i', 'input.mp4',
'-ss', startTime.toString(),
'-to', endTime.toString(),
'-c', 'copy',
'output.mp4'
);
// 读取结果
const data = this.ffmpeg.FS('readFile', 'output.mp4');
const croppedBlob = new Blob([data.buffer], { type: 'video/mp4' });
// 创建新视频元素
const croppedUrl = URL.createObjectURL(croppedBlob);
const croppedVideo = {
id: `cropped_${Date.now()}`,
type: 'video/mp4',
url: croppedUrl,
name: `${this.state.currentProject.sourceVideo.name}_裁剪版`,
duration: endTime - startTime
};
// 添加到媒体库
this.addMediaToLibrary(croppedVideo);
// 加载裁剪后的视频
this.loadVideo(croppedVideo);
this.showMessage('视频裁剪完成', 'success');
} catch (error) {
console.error('裁剪失败:', error);
this.showMessage('视频裁剪失败', 'error');
}
}
// 其他辅助方法
togglePlay() {
if (this.state.isPlaying) {
this.video.pause();
} else {
this.video.play();
}
this.state.isPlaying = !this.state.isPlaying;
const playBtn = document.getElementById('btn-play');
playBtn.innerHTML = this.state.isPlaying ?
'<i class="icon-pause"></i>' :
'<i class="icon-play"></i>';
}
updateTimeDisplay() {
document.getElementById('current-time').textContent =
this.formatTime(this.state.currentTime);
}
updateDurationDisplay() {
document.getElementById('duration').textContent =
this.formatTime(this.state.duration);
}
formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
showMessage(message, type = 'info') {
// 创建消息元素
const messageEl = document.createElement('div');
messageEl.className = `editor-message editor-message-${type}`;
messageEl.textContent = message;
// 添加到容器
this.container.appendChild(messageEl);
// 3秒后移除
setTimeout(() => {
if (messageEl.parentNode) {
messageEl.parentNode.removeChild(messageEl);
}
}, 3000);
}
// 初始化时间轴
initTimeline() {
// 创建时间刻度
this.renderTimelineRuler();
// 设置拖动事件
this.setupTimelineEvents();
}
renderTimelineRuler() {
const ruler = document.querySelector('.timeline-ruler');
if (!ruler) return;
const totalSeconds = Math.ceil(this.state.duration);
const pixelsPerSecond = 50; // 每秒钟50像素
for (let i = 0; i <= totalSeconds; i += 5) {
const tick = document.createElement('div');
tick.className = 'timeline-tick';
tick.style.left = `${i * pixelsPerSecond}px`;
const label = document.createElement('span');
label.className = 'timeline-label';
label.textContent = this.formatTime(i);
tick.appendChild(label);
ruler.appendChild(tick);
}
}
setupTimelineEvents() {
const timeline = document.querySelector('.timeline-track');
if (!timeline) return;
timeline.addEventListener('click', (e) => {
const rect = timeline.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const pixelsPerSecond = 50;
const time = clickX / pixelsPerSecond;
this.video.currentTime = Math.min(time, this.state.duration);
this.state.currentTime = this.video.currentTime;
});
}
updateTimelinePosition() {
const scrubber = document.querySelector('.timeline-scrubber');
if (!scrubber) return;
const progress = (this.state.currentTime / this.state.duration) * 100;
scrubber.style.left = `${progress}%`;
}
addToTimeline(element) {
const timelineTracks = document.querySelector('.timeline-tracks');
if (!timelineTracks) return;
const track = document.createElement('div');
track.className = 'timeline-element';
track.dataset.id = element.id;
// 计算位置和宽度
const pixelsPerSecond = 50;
const left = element.startTime * pixelsPerSecond;
const width = element.duration * pixelsPerSecond;
track.style.left = `${left}px`;
track.style.width = `${width}px`;
// 根据类型设置样式
if (element.type === 'text') {
track.classList.add('text-element');
track.innerHTML = `<span class="element-label">T</span>`;
}
timelineTracks.appendChild(track);
}
renderMediaLibrary() {
const mediaLibrary = document.querySelector('.media-library');
if (!mediaLibrary) return;
mediaLibrary.innerHTML = '';
this.state.mediaElements.forEach(media => {
const item = document.createElement('div');
item.className = 'media-item';
item.dataset.id = media.id;
item.innerHTML = `
<div class="media-thumbnail">
${media.type.startsWith('video/') ?
`<i class="icon-video"></i>` :
`<i class="icon-audio"></i>`}
</div>
<div class="media-info">
<div class="media-name">${media.name}</div>
${media.duration ?
`<div class="media-duration">${this.formatTime(media.duration)}</div>` : ''}
</div>
`;
item.addEventListener('click', () => {
if (media.type.startsWith('video/')) {
this.loadVideo(media);
} else if (media.type.startsWith('audio/')) {
this.addAudioToProject(media);
}
});
mediaLibrary.appendChild(item);
});
}
}
// 初始化编辑器
document.addEventListener('DOMContentLoaded', () => {
window.videoEditor = new VideoEditor({
ajaxUrl: videoEditorConfig.ajaxUrl,
nonce: videoEditorConfig.nonce,
userId: videoEditorConfig.userId
});
});
## 第四部分:后端API与数据处理
### 4.1 实现REST API端点
扩展WordPress插件类,添加REST API支持:
public function register_rest_routes() {
// 项目管理端点
register_rest_route('video-editor/v1', '/projects', array(
array(
'methods' => 'GET',
'callback' => array($this, 'get_user_projects'),
'permission_callback' => array($this, 'check_user_permission'),
),
array(
'methods' => 'POST',
'callback' => array($this, 'create_project'),
'permission_callback' => array($this, 'check_user_permission'),
),
));
register_rest_route('video-editor/v1', '/projects/(?P<id>d+)', array(
array(
'methods' => 'GET',
'callback' => array($this, 'get_project'),
'permission_callback' => array($this, 'check_user_permission'),
),
array(
'methods' => 'PUT',
'callback' => array($this, 'update_project'),
'permission_callback' => array($this, 'check_user_permission'),
),
array(
'methods' => 'DELETE',
'callback' => array($this, 'delete_project'),
'permission_callback' => array($this, 'check_user_permission'),
),
));
// 文件上传端点
register_rest_route('video-editor/v1', '/upload', array(
'methods' => 'POST',
'callback' => array($this, 'handle_file_upload'),
'permission_callback' => array($this, 'check_user_permission'),
));
// 视频处理端点
register_rest_route('video-editor/v1', '/process', array(
'methods' => 'POST',
'callback' => array($this, 'process_video'),
'permission_callback' => array($this, 'check_user_permission'),
));
}
public function check_user_permission($request) {
return is_user_logged_in();
}
public function handle_file_upload($request) {
// 检查文件上传
if (empty($_FILES['file'])) {
return new WP_Error('no_file', '没有上传文件', array('status' => 400));
}
$file = $_FILES['file'];
// 检查文件类型
$allowed_types = get_option('video_editor_allowed_formats', 'mp4,webm,mov,avi');
$allowed_types = explode(',', $allowed_types);
$file_ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($file_ext, $allowed_types)) {
return new WP_Error('invalid_type', '不支持的文件格式', array('status' => 400));
}
// 检查文件大小
$max_size = get_option('video_editor_max_upload_size', 500) * 1024 * 1024;
if ($file['size'] > $max_size) {
return new WP_Error('file_too_large', '文件太大', array('status' => 400));
}
// 处理上传
require_once(ABSPATH . 'wp-admin/includes/file.php');
require_once(ABSPATH . 'wp-admin/includes/media.php');
require_once(ABSPATH . 'wp-admin/includes/image.php');
$upload_overrides = array('test_form' => false);
$uploaded_file = wp_handle_upload($file, $upload_overrides);
if (isset($uploaded_file['error'])) {
return new WP_Error('upload_error', $uploaded_file['error'], array('status' => 500));
}
// 创建附件
$attachment = array(
'post_mime_type' => $uploaded_file['type'],
'post_title' => preg_replace('/.[^.]+$/', '', basename($uploaded_file['file'])),
'post_content' => '',
'post_status' => 'inherit',
'guid' => $uploaded_file['url']
);
$attach_id = wp_insert_attachment($attachment, $uploaded_file['file']);
// 生成元数据
$attach_data = wp_generate_attachment_metadata($attach_id, $uploaded_file['file']);
wp_update_attachment_metadata($attach_id, $attach_data);
// 获取视频时长(如果可能)
$duration = 0;
if (strpos($uploaded_file['type'], 'video/') === 0) {
$duration = $this->get_video_duration($uploaded_file['file']);
}
// 生成缩略图
$thumbnail_url = '';
if (strpos($uploaded_file['type'], 'video/') === 0) {
$thumbnail_url = $this->generate_video_thumbnail($attach_id, $uploaded_file['file']);
}
return rest_ensure_response(array(
'success' => true,
'data' => array(
'id' => $attach_id,
'url' => $uploaded_file['url'],
'type' => $uploaded_file['type'],
'name' => basename($uploaded_file['file']),
'duration' => $duration,
'thumbnail' => $thumbnail_url
)
));
}
private function get_video_duration($file_path) {
// 使用FFmpeg获取视频时长
if (function_exists('shell_exec')) {
$cmd = "ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 " . escapeshellarg($file_path);
$duration = shell_exec($cmd);
return floatval($duration);
}
return 0;
}
private function generate_video_thumbnail($attachment_id, $file_path) {
// 使用FFmpeg生成缩略图
$upload_dir = wp_upload_dir();
$thumbnail_path = $upload_dir['path'] . '/thumb_' . $attachment_id . '.jpg';
if (function_exists('shell_exec')) {
$cmd = "ffmpeg -i " . escapeshellarg($file_path) . " -ss 00:00:01 -vframes 1 -q:v 2 " . escapeshellarg($thumbnail_path) . " 2>&1";
shell_exec($cmd);
if (file_exists($thumbnail_path)) {
// 将缩略图添加到媒体库
$thumbnail_attachment = array(
'post_mime_type' => 'image/jpeg',
'post_title'
