前言
起因是在服务器上装了wordpress-studio,然后想试试把平时拍的照片和视频放上去
但是吧,相机拍摄的4k视频,码率实在是有些高,30秒的视频就500M了,直接传到wordpress然后用自带的视频区块,加载速度惨不忍睹
于是乎找到万能的gpt,写一个插件让能支持m3u8切片视频播放的插件
不说废话了,直接开始吧
项目结构
│ hls-block.php
│ package-lock.json
│ package.json
│
├─assets
│ hls-block.js
│ hls.min.js
│ plyr.min.css
│ plyr.min.js
│
└─src
edit.js
index.js
style.css
说明:
到 wp-content/plugins 新建一个目录存放插件
直接上代码了
hls-block.php
<?php
/**
* Plugin Name: HLS Video Block
* Description: Gutenberg 区块,支持 m3u8/HLS 视频播放和分辨率切换
* Version: 1.0
* Author: chatgpt
*/
if ( ! defined( 'ABSPATH' ) ) exit;
// 注册区块
function hls_video_block_register_block() {
wp_register_script(
'hls-video-block-editor',
plugins_url( 'build/index.js', __FILE__ ),
array( 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n' ),
filemtime( plugin_dir_path( __FILE__ ) . 'build/index.js' )
);
register_block_type( 'hls/video-block', array(
'editor_script' => 'hls-video-block-editor',
'render_callback' => 'hls_video_block_render',
'attributes' => array(
'file' => array(
'type' => 'string',
'default' => '',
),
'width' => array(
'type' => 'number',
'default' => 800,
),
'height' => array(
'type' => 'number',
'default' => 450,
),
),
) );
}
add_action( 'init', 'hls_video_block_register_block' );
// 区块前端渲染
function hls_video_block_render( $attributes ) {
if ( empty( $attributes['file'] ) ) {
return '<div style="color:red;">未指定视频文件</div>';
}
$src0 = 'uploads/hls/' . $attributes['file'];
$src = esc_url( content_url( 'uploads/hls/' . $attributes['file'] ) );
$width = intval( $attributes['width'] );
$height = intval( $attributes['height'] );
$player_id = 'hls-block-player-' . uniqid();
// 加载 video.js 和 hls.js
wp_enqueue_script( 'plyrjs', plugins_url( 'assets/plyr.min.js', __FILE__ ), array(), null, true );
wp_enqueue_style( 'plyr-css', plugins_url( 'assets/plyr.min.css', __FILE__ ) );
wp_enqueue_script( 'hlsjs', plugins_url( 'assets/hls.min.js', __FILE__ ), array(), null, true );
wp_enqueue_script( 'hls-block-js', plugins_url( 'assets/hls-block.js', __FILE__ ), array(), null, true );
ob_start();
?>
<figure class='wp-block-video'>
<video id="<?php echo esc_attr( $player_id ); ?>" controls crossorigin playsinline width="<?php echo esc_attr( $width ); ?>" height="<?php echo esc_attr( $height ); ?>">
<source type="application/x-mpegURL" src="<?php echo esc_url( $src ); ?>">
</video>
</firgure>
<script>
document.addEventListener('DOMContentLoaded', function() {
if(window.hlss === undefined) {
window.hlss = {};
}
const vidid = '<?php echo esc_js( $player_id ); ?>';
const video = document.getElementById(vidid);
const source = video.getElementsByTagName('source')[0].src;
const defaultOptions = {};
if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource(source);
hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) {
const availableQualities = hls.levels.map((l) => l.height)
defaultOptions.quality = {
default: availableQualities[0],
options: availableQualities,
forced: true,
onChange: (e) => updateQuality(e, vidid),
};
const player = new Plyr(video, defaultOptions);
});
hls.attachMedia(video);
window.hlss[vidid] = hls;
} else {
const player = new Plyr(video, defaultOptions);
}
});
</script>
<?php
return ob_get_clean();
}
// 提供 REST API 获取 m3u8 文件列表
add_action( 'rest_api_init', function () {
register_rest_route( 'hls-block/v1', '/files', array(
'methods' => 'GET',
'callback' => function () {
$dir = WP_CONTENT_DIR . '/uploads/hls/';
$files = array();
if ( is_dir( $dir ) ) {
foreach ( glob( $dir . '*.m3u8' ) as $file ) {
$files[] = basename( $file );
}
}
return rest_ensure_response( $files );
},
'permission_callback' => '__return_true',
) );
} );
src/index.js
import { registerBlockType } from '@wordpress/blocks';
import Edit from './edit';
registerBlockType('hls/video-block', {
title: '视频流',
icon: 'format-video',
category: 'media',
attributes: {
file: { type: 'string', default: '' },
width: { type: 'number', default: 800 },
height: { type: 'number', default: 450 },
},
edit: Edit,
save: () => null,
style: './style.css',
});
src/edit.js
import { useEffect, useState } from '@wordpress/element';
import { InspectorControls } from '@wordpress/block-editor';
import { PanelBody, SelectControl, TextControl, RangeControl } from '@wordpress/components';
export default function Edit({ attributes, setAttributes, className }) {
const { file, width, height } = attributes;
const [files, setFiles] = useState([]);
useEffect(() => {
fetch('/wp-json/hls-block/v1/files')
.then(res => res.json())
.then(setFiles);
}, []);
// 处理下拉选择
const handleSelectChange = (val) => {
setAttributes({ file: val });
};
// 处理文本输入
const handleInputChange = (val) => {
setAttributes({ file: val });
};
return (
<>
<InspectorControls>
<PanelBody title="视频设置" initialOpen={true}>
<SelectControl
label="选择m3u8文件"
value={file}
options={[
{ label: '请选择', value: '' },
...files.map(f => ({ label: f, value: f }))
]}
onChange={handleSelectChange}
help="可从下拉选择,或在下方输入自定义文件名"
/>
<TextControl
label="或输入文件名"
value={file}
onChange={handleInputChange}
placeholder="如 example.m3u8"
help="支持直接输入文件名"
/>
<RangeControl
label="宽度"
value={width}
onChange={val => setAttributes({ width: val })}
min={320}
max={1920}
/>
<RangeControl
label="高度"
value={height}
onChange={val => setAttributes({ height: val })}
min={180}
max={1080}
/>
</PanelBody>
</InspectorControls>
<div className={`hls-video-block ${className || ''}`} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
{file ? (
<div style={{ border: '1px solid #ccc', padding: 10 }}>
<strong>{ `${file}` }</strong>
{ <video
controls
width={width}
height={height}
style={{ background: '#000', display: 'block', margin: '0 auto' }}
>
{/* <source src={`/wp-content/uploads/hls/${file}`} type="application/x-mpegURL" /> */}
</video> }
{/* <div style={{ fontSize: 12, color: '#888', textAlign: 'center' }}>保存后可切换分辨率</div> */}
</div>
) : (
<div style={{ color: 'red', textAlign: 'center' }}>请选择或输入 m3u8 文件</div>
)}
</div>
</>
);
}
src/style.css
.hls-video-block {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 2em 0;
}
.hls-video-block video {
display: block;
margin: 0 auto;
background: #000;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.hls-video-block .hls-video-preview {
border: 1px solid #ccc;
padding: 10px;
margin-bottom: 8px;
background: #fafafa;
border-radius: 6px;
}
.hls-video-block .hls-video-help {
font-size: 12px;
color: #888;
text-align: center;
}
.hls-video-block .hls-video-error {
color: #d63638;
text-align: center;
font-weight: bold;
}
assets/hls-block.js
function updateQuality(newQuality, vidid) {
window.hlss[vidid].levels.forEach((level, levelIndex) => {
if (level.height === newQuality) {
window.hlss[vidid].currentLevel = levelIndex;
}
});
}
生成build
到插件根目录下
npm install wordpress/scripts
npx wp-scripts build
package.json参考
{
"name": "hls-video-block",
"version": "1.0.0",
"description": "Gutenberg block for HLS/m3u8 video playback",
"main": "build/index.js",
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start"
},
"devDependencies": {
"@wordpress/scripts": "^26.0.0"
}
}
assets下面其他的js和css手动下载后放入
python视频切片脚本参考
import os
import json
import subprocess
from pathlib import Path
import ffmpeg
import uuid
INPUT_DIR = 'input'
OUTPUT_DIR = 'output'
SEGMENT_TIME = 10 # 每段切成10秒
RESOLUTION_MAP = {
"480p": 480,
"720p": 720,
"1080p": 1080,
"1440p": 1440,
}
RESOLUTION_BITRATE_MAP = {
240: 500_000,
360: 950_000,
480: 1_400_000,
720: 2_800_000,
1080: 5_500_000,
1440: 8_200_000,
2160: 14_200_000
}
def estimate_bandwidth(width, height):
h = min(width, height)
candidates = sorted(RESOLUTION_BITRATE_MAP.items())
for res_h, bw in candidates:
if h <= res_h:
return bw
return RESOLUTION_BITRATE_MAP[2160]
def get_video_resolution(filepath):
probe = ffmpeg.probe(filepath)
video_stream = next(stream for stream in probe['streams'] if stream['codec_type'] == 'video')
width = video_stream['width']
height = video_stream['height']
return width, height
# ✅ 新增:根据 rotate 构造滤镜
def build_ffmpeg_rotate_filter(rotate):
if rotate == 90:
return "transpose=1"
elif rotate == 180:
return "hflip,vflip"
elif rotate == 270:
return "transpose=2"
else:
return None
def build_ffmpeg_scale_filter(width, height, rotate=None):
rotate_filter = build_ffmpeg_rotate_filter(rotate)
scale = f"scale={width}:{height}"
if rotate_filter:
# return f"{rotate_filter},{scale}"
return f"{scale},{rotate_filter}"
else:
return scale
# ✅ 修改:支持 rotate 的压缩转码函数
def transcode_and_segment(input_path, output_dir, resolution_label, resolution_value, rotate=0):
os.makedirs(output_dir, exist_ok=True)
scale_filter = build_ffmpeg_scale_filter(-2, resolution_value, rotate)
out_ts_pattern = os.path.join(output_dir, f"{resolution_label}_%04d.ts")
out_m3u8 = os.path.join(output_dir, f"{resolution_label}.m3u8")
cmd = [
'ffmpeg', '-y', '-i', input_path,
'-vf', scale_filter,
'-c:a', 'aac', '-ar', '48000', '-b:a', '128k',
'-c:v', 'h264_amf', '-b:v', '2500k',
'-g', '48', '-keyint_min', '48', '-sc_threshold', '0',
'-hls_time', str(SEGMENT_TIME),
'-hls_playlist_type', 'vod',
'-hls_segment_filename', out_ts_pattern,
out_m3u8
]
subprocess.run(cmd, check=True)
actual_width, actual_height = get_video_resolution(out_ts_pattern.replace("_%04d.ts", "_0000.ts"))
return actual_width, actual_height
# ✅ 修改:原始视频也支持 rotate
def segment_source(input_path, output_dir, rotate=0):
os.makedirs(output_dir, exist_ok=True)
out_ts_pattern = os.path.join(output_dir, "source_%04d.ts")
out_m3u8 = os.path.join(output_dir, "source.m3u8")
cmd = [
'ffmpeg', '-y', '-i', input_path,
]
rotate_filter = build_ffmpeg_rotate_filter(rotate)
if rotate_filter:
cmd += ['-vf', rotate_filter]
cmd += ['-c:v', 'h264_amf']
else:
cmd += ['-c:v', 'copy']
cmd += [
'-c:a', 'aac', '-ar', '48000', '-b:a', '128k',
'-hls_time', str(SEGMENT_TIME),
'-hls_playlist_type', 'vod',
'-hls_segment_filename', out_ts_pattern,
out_m3u8
]
subprocess.run(cmd, check=True)
return get_video_resolution(out_ts_pattern.replace("_%04d.ts", "_0000.ts"))
def write_master_playlist(output_path, stream_infos, v_uuid):
with open(output_path, 'w') as f:
f.write('#EXTM3U\n')
for label, resolution, filename in stream_infos:
width, height = resolution
w = max(width, height)
h = min(width, height)
bandwidth = estimate_bandwidth(width, height)
f.write(f'#EXT-X-STREAM-INF:BANDWIDTH={bandwidth},RESOLUTION={w}x{h}\n')
f.write(f'{v_uuid}/{filename}\n')
def process_json(json_path):
with open(json_path, 'r', encoding='utf-8') as f:
config = json.load(f)
input_file = config['input_file']
v_uuid = config.get('uuid') or str(uuid.uuid4())
rotate = int(config.get('rotate', 0))
source = bool(config.get('source', True))
title = config['title']
targets = config['target']
input_path = os.path.join(INPUT_DIR, input_file)
output_base = os.path.join(OUTPUT_DIR, v_uuid)
title_m3u8_path = os.path.join(OUTPUT_DIR, f"{title}.m3u8")
print(f"处理视频: {input_file}")
width, height = get_video_resolution(input_path)
short_edge = min(width, height)
stream_infos = []
# 源视频切片(支持旋转)
if source:
source_resolution = segment_source(input_path, output_base, rotate)
stream_infos.append(("source", source_resolution, "source.m3u8"))
# 清晰度转码(支持旋转)
for res_label in targets:
res_value = RESOLUTION_MAP.get(res_label)
if res_value and res_value < short_edge:
try:
actual_res = transcode_and_segment(input_path, output_base, res_label, res_value, rotate)
stream_infos.append((res_label, actual_res, f"{res_label}.m3u8"))
except Exception as e:
print(f"转码 {res_label} 出错: {e}")
else:
print(f"跳过分辨率 {res_label},因为源视频短边为 {short_edge}")
write_master_playlist(title_m3u8_path, stream_infos, v_uuid)
print(f"✅ 已生成主播放列表: {title_m3u8_path}")
os.rename(json_path, f"{json_path}.1")
def main():
json_files = Path(INPUT_DIR).glob("*.json")
for json_file in json_files:
try:
process_json(json_file)
except Exception as e:
print(f"处理 {json_file} 出错: {e}")
if __name__ == "__main__":
main()
建input和output文件夹,然后input中放入需要切片的视频参数
{
"input_file": "E:\\temp\\DSC_2682.MOV",
"title": "视频标题",
"rotate": 270, // 泥坑竖屏视频不会自动旋转,所以要加上这行,如果视频方向没问题就不用加
"target": [ // 目标分辨率
"720p",
"1080p"
]
}
然后运行py脚本,output下面会生成 视频标题.m3u8 和一个 uuid 文件夹,
这些文件剪切到wordpress根目录下面的 wp-content/uploads/hls
文章编辑页面,添加区块,旋转视频流
然后点击区块,到属性页面,选择视频文件
发布后,播放时可以选择分辨率