Files
PCAdmissionOfficer/hevc-to-h264-player.js
DESKTOP-RQ919RC\Pc 047ddd57f6 feat: 添加HEVC转H.264播放器和打字机效果页面
添加HEVC转H.264播放器功能,包含文件选择、转码进度显示和视频播放
新增打字机效果展示页面,使用Vue实现逐字显示效果
2025-07-25 18:49:11 +08:00

200 lines
7.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class HevcToH264Player {
constructor() {
// 关键修正:使用正确的核心库路径(注意文件后缀和目录)
this.config = {
ffmpegScriptUrl: "https://cdnjs.cloudflare.com/ajax/libs/ffmpeg/0.12.15/umd/ffmpeg.min.js",
// 核心库必须使用完整路径包含wasm文件的正确位置
ffmpegCorePath: "https://cdnjs.cloudflare.com/ajax/libs/ffmpeg-core/0.12.10/esm/ffmpeg-core.js",
};
// DOM元素
this.fileInput = document.getElementById("hevcFileInput");
this.statusEl = document.getElementById("conversionStatus");
this.progressBar = document.getElementById("progressBar");
this.videoContainer = document.getElementById("videoContainer");
// 状态变量
this.ffmpeg = null;
this.isLoaded = false;
this.coreLoaded = false;
// 初始化
this.bindEvents();
this.createVideoElement();
}
// 创建视频元素
createVideoElement() {
this.video = document.createElement("video");
this.video.controls = true;
this.video.style.width = "100%";
this.video.style.display = "none";
this.videoContainer.appendChild(this.video);
this.loadingDiv = document.createElement("div");
this.loadingDiv.style.cssText = `
position: absolute; top:0; left:0; width:100%; height:100%;
background:rgba(0,0,0,0.7); color:white; display:flex;
flex-direction:column; justify-content:center; align-items:center;
`;
this.loadingDiv.innerHTML = '<div class="spinner"></div><div id="loadingText">准备中...</div>';
this.videoContainer.appendChild(this.loadingDiv);
this.loadingText = this.loadingDiv.querySelector("#loadingText");
// 添加动画样式
const style = document.createElement("style");
style.textContent = `
.spinner { border:4px solid rgba(255,255,255,0.3); border-top:4px solid white;
border-radius:50%; width:40px; height:40px; animation:spin 1s linear infinite; }
@keyframes spin { 0% { transform:rotate(0deg); } 100% { transform:rotate(360deg); } }
`;
document.head.appendChild(style);
}
// 绑定事件
bindEvents() {
this.fileInput.addEventListener("change", (e) => {
if (e.target.files.length) {
this.convert(e.target.files[0]);
}
});
}
// 加载主库
async loadMainLibrary() {
return new Promise((resolve, reject) => {
// if (window.FFmpeg && window.FFmpeg.createFFmpeg) {
// resolve(window.FFmpeg);
// return;
// }
const script = document.createElement("script");
script.src = this.config.ffmpegScriptUrl;
script.type = "text/javascript";
script.onload = () => {
if (window.FFmpeg && window.FFmpeg.createFFmpeg) {
resolve(window.FFmpeg);
} else {
reject(new Error("主库加载但未找到createFFmpeg方法版本不兼容"));
}
};
script.onerror = () => reject(new Error("主库加载失败网络或URL错误"));
document.head.appendChild(script);
});
}
// 验证核心库是否加载(关键修正)
checkCoreLibraryLoaded() {
return new Promise((resolve) => {
const checkInterval = setInterval(() => {
// 检查核心库的WASM文件是否已加载
const wasmLoaded = document.querySelectorAll(`script[src*="ffmpeg-core.wasm"]`).length > 0;
if (wasmLoaded) {
clearInterval(checkInterval);
this.coreLoaded = true;
resolve();
}
}, 200);
// 核心库加载超时30秒
setTimeout(() => {
clearInterval(checkInterval);
resolve(); // 超时也继续让FFmpeg自己处理错误
}, 30000);
});
}
// 初始化FFmpeg
async initFFmpeg() {
if (this.ffmpeg && this.isLoaded) return;
this.updateStatus("加载主库...");
const FFmpeg = await this.loadMainLibrary();
console.log("FFmpeg", FFmpeg);
this.updateStatus("初始化FFmpeg实例...");
this.ffmpeg = FFmpeg.createFFmpeg({
log: true,
corePath: this.config.ffmpegCorePath,
});
this.updateStatus("加载核心库约20MB...");
// 开始加载核心库
const loadPromise = this.ffmpeg.load();
// 等待核心库加载迹象
// await this.checkCoreLibraryLoaded();
// 等待加载完成
await loadPromise;
this.isLoaded = true;
this.updateStatus("FFmpeg完全加载成功");
}
// 更新状态
updateStatus(text) {
if (this.statusEl) this.statusEl.textContent = text;
if (this.loadingText) this.loadingText.textContent = text;
}
// 转码主逻辑
async convert(file) {
this.updateStatus("准备转码...");
this.progressBar.style.width = "0%";
this.loadingDiv.style.display = "flex";
this.video.style.display = "none";
try {
await this.initFFmpeg();
// 写入文件
const inputName = "input.hevc";
const outputName = "output.mp4";
this.ffmpeg.FS("writeFile", inputName, new Uint8Array(await file.arrayBuffer()));
// 转码进度
this.ffmpeg.setProgress(({ ratio }) => {
const progress = Math.round(ratio * 100);
this.progressBar.style.width = `${progress}%`;
this.updateStatus(`转码中:${progress}%`);
});
// 执行转码
this.updateStatus("开始转码...");
await this.ffmpeg.run("-i", inputName, "-c:v", "libx264", "-crf", "23", "-preset", "medium", "-c:a", "aac", "-b:a", "128k", outputName);
// 播放结果
const data = this.ffmpeg.FS("readFile", outputName);
const url = URL.createObjectURL(new Blob([data.buffer], { type: "video/mp4" }));
this.video.src = url;
this.video.onloadedmetadata = () => {
this.video.style.display = "block";
this.loadingDiv.style.display = "none";
this.updateStatus("转码完成,正在播放");
this.video.play().catch(() => {});
};
} catch (error) {
// 核心库未加载的特殊处理
if (!this.coreLoaded) {
error.message = "核心库加载失败请检查网络或尝试更换CDN";
}
this.updateStatus(`转码失败:${error.message}`);
// console.error("错误详情:", error);
}
}
}
// 页面加载后初始化
document.addEventListener("DOMContentLoaded", () => {
// 确保DOM元素存在
if (["hevcFileInput", "conversionStatus", "progressBar", "videoContainer"].every((id) => document.getElementById(id))) {
window.hevcPlayer = new HevcToH264Player();
} else {
console.error("缺少必要的DOM元素");
}
});