CMP4 JW Player 皮肤 · LMP34 适配版
外观升级说明:本皮肤在原 CenFun CMP4 迷你皮肤基础上做了外观升级,成为全新外观的 HTML/CSS 重制版。原版依赖两个 Flash SWF 背景(console.swf / media.swf)已全部替换为纯 CSS 渐变绘制,按钮雪碧图 PNG 原样保留并由内核状态机换帧,坐标从固定像素改为流式 100P / 26R,由 skin.js 的 onLayout 钩子在每次视口变化时自动撑满。原作者信息完整保留,见文末。
⚠️ 频谱插件放置位置 (关键)
LMP34 内核有两种插件层,频谱类插件必须挂对位置才能在画面上看到:
- ✅ 正确 — 写在
plugins里,渲染在视频层之上,频谱画布与播放器同区域 - ❌ 错误 — 写在
bg_plugins里,该层位于视频层下方,会被封面图/视频完全挡住,导致频谱"写了配置但看不见"
播放器配置示例
完整 HTML 部署 + 单皮肤加载 cmp4-jwplayer,直接复制替换即可:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="referrer" content="never">
<title>LMP34 Player</title>
</head>
<body>
<div style="position:fixed;top:0;left:0;width:100%;height:100%;">
<div id="lmp34-player" style="width:100%;height:100%;"></div>
</div>
<script src="https://lmp34.cnlog.cc/lmp34-embed.js"></script>
<script>
LMP34.init({
container: 'lmp34-player',
name: 'CMP4 JW Player Demo',
link: 'https://www.cnlog.cc',
// ========== 皮肤(单皮肤,指向 cmp4-jwplayer 目录) ==========
skin: 'skins/cmp4-jwplayer',
// ========== 播放列表 ==========
lists: 'list.xml',
auto_play: 1,
// ========== ✅ 视频层插件(可见层) ==========
// 频谱类插件必须写在 plugins,不能写 bg_plugins
plugins: 'waveform,ktv',
// ========== ❌ 背景插件层(叠在视频层下面,频谱看不见) ==========
bg_plugins: 'marquee',
// ========== 频谱(spectrum_id 控制样式 1-18,0=关闭) ==========
spectrum_id: 1,
// ========== Effects 视觉特效 ==========
effects: 'snow',
// ========== 代理 ==========
proxy: ''
});
</script>
</body>
</html>
皮肤源码 · skin.css
216 行样式,覆盖媒体区、底部控制条、进度条/音量条、歌词窗、列表窗、自定义滚动条。所有资源图路径留空(thumb_src="" track_src=""),改由 CSS 绘制,避免原 CMP4 的 PNG 在高清屏下糊。
/* JW Player 皮肤 - HTML/CSS 重制版(LMP34)
* 原 console.swf / media.swf 两个 SWF 背景,全部改由本文件的 CSS 渐变绘制。
* 按钮雪碧图 PNG(bt_play / bt_stop / bt_mute / bt_fullscreen)原样保留,由内核状态机换帧。
* bt_prev / bt_next / bt_list / bt_lrc / bt_option 无图,用伪元素画字形。
*/
/* ---------- 媒体区(原 media.swf 黑底) ---------- */
.lmp-window-media {
background: #000 !important;
}
.lmp-window-media .lmp-video-container {
background: #000;
}
.lmp-window-media .lmp-cover {
object-fit: contain;
}
/* ---------- 底部控制条(原 console.swf 灰色渐变) ---------- */
.lmp-window-console {
background: linear-gradient(#8c8c8c 0%, #6b6b6b 45%, #4f4f4f 55%, #3a3a3a 100%) !important;
border-top: 1px solid #1c1c1c !important;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .28) !important;
}
/* ---------- 控制条按钮通用 ---------- */
.lmp-window-console .lmp-btn {
cursor: pointer;
}
/* ---------- 无图按钮:伪元素画字形 ---------- */
.lmp-bt_prev::before,
.lmp-bt_next::before,
.lmp-bt_list::before,
.lmp-bt_lrc::before,
.lmp-bt_option::before {
display: block;
line-height: 18px;
text-align: center;
font-size: 11px;
font-family: Verdana, Arial, sans-serif;
color: #e8e8e8;
text-shadow: 0 -1px 0 rgba(0, 0, 0, .6);
pointer-events: none;
cursor: pointer;
}
.lmp-bt_prev::before { content: "\25C0\25C0"; font-size: 9px; letter-spacing: -2px; }
.lmp-bt_next::before { content: "\25B6\25B6"; font-size: 9px; letter-spacing: -2px; }
.lmp-bt_list::before { content: "\2630"; font-size: 13px; }
.lmp-bt_lrc::before { content: "\8BCD"; font-size: 12px; }
.lmp-bt_option::before { content: "i"; font-style: italic; font-weight: bold; font-size: 12px; }
.lmp-bt_prev:hover::before,
.lmp-bt_next:hover::before,
.lmp-bt_list:hover::before,
.lmp-bt_lrc:hover::before,
.lmp-bt_option:hover::before {
color: #ff9900;
text-shadow: 0 0 4px rgba(255, 153, 0, .5);
}
/* ---------- 进度条(原 progress_track/thumb.png 改 CSS 绘制) ---------- */
.lmp-window-console .lmp-progress {
background: linear-gradient(#5e5e5e, #262626) !important;
background-image: linear-gradient(#5e5e5e, #262626) !important;
border-radius: 5px !important;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, .75), 0 1px 0 rgba(255, 255, 255, .14) !important;
cursor: pointer;
overflow: hidden;
}
/* 已播放高亮:由 skin.js 注册 media_time_update 钩子驱动宽度 */
.lmp-window-console .lmp-progress-fill {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 0;
border-radius: 5px;
background: linear-gradient(#a8f29a, #5ecf5e 45%, #34a53a);
box-shadow: 0 0 6px rgba(76, 190, 80, .5);
pointer-events: none;
}
/* 滑块:亮色圆角小方块(替代原 8x7 灰色 PNG,显式定尺寸保证内核居中公式正确) */
.lmp-window-console .lmp-progress-thumb {
background-image: none !important;
background: linear-gradient(#ffffff, #c9c9c9) !important;
width: 10px !important;
height: 10px !important;
top: 0 !important;
border-radius: 3px !important;
box-shadow: 0 0 4px rgba(0, 0, 0, .8), inset 0 1px 0 #fff !important;
}
.lmp-window-console .lmp-volume {
background: rgba(0, 0, 0, .55) !important;
background-image: none !important;
border-radius: 2px;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, .8), 0 1px 0 rgba(255, 255, 255, .15);
cursor: pointer;
overflow: hidden;
}
.lmp-window-console .lmp-volume-thumb {
background: linear-gradient(#f5f5f5, #b8b8b8) !important;
width: 6px !important;
height: 10px !important;
top: 0 !important;
border-radius: 2px !important;
box-shadow: 0 0 3px rgba(0, 0, 0, .6) !important;
}
/* ---------- 歌词窗 ---------- */
.lmp-window-lrc {
background: rgba(0, 0, 0, .32) !important;
pointer-events: none;
}
.lmp-window-lrc .lmp-lrc-text {
padding: 0 16px;
box-sizing: border-box;
}
.lmp-lrc-line {
padding: 2px 0;
transition: color .2s;
text-shadow: 0 1px 2px rgba(0, 0, 0, .85);
}
.lmp-lrc-line.lmp-active {
color: #ff9900;
font-weight: bold;
}
/* ---------- 列表 / 设置窗 ---------- */
.lmp-window-list,
.lmp-window-option {
background: rgba(24, 24, 24, .97) !important;
border: 1px solid #3c3c3c !important;
box-shadow: 0 2px 14px rgba(0, 0, 0, .6) !important;
}
.lmp-tree-item {
color: #cccccc;
}
.lmp-tree-folder {
color: #e0e0e0;
}
.lmp-tree-item:hover,
.lmp-tree-folder:hover {
color: #ffffff;
background: rgba(255, 255, 255, .08);
}
.lmp-tree-item.lmp-active,
.lmp-tree-folder.lmp-active {
color: #ff9900 !important;
background: rgba(255, 153, 0, .16) !important;
text-shadow: none;
}
/* ---------- 列表滚动条 ---------- */
.lmp-window-list .lmp-tree,
.lmp-window-option .lmp-tree {
scrollbar-width: thin;
scrollbar-color: #4a4a4a rgba(255, 255, 255, .05);
}
@supports selector(::-webkit-scrollbar) {
.lmp-window-list .lmp-tree,
.lmp-window-option .lmp-tree {
scrollbar-width: auto;
scrollbar-color: auto;
}
}
.lmp-window-list .lmp-tree::-webkit-scrollbar,
.lmp-window-option .lmp-tree::-webkit-scrollbar {
width: 10px;
height: 10px;
}
.lmp-window-list .lmp-tree::-webkit-scrollbar-track,
.lmp-window-option .lmp-tree::-webkit-scrollbar-track {
background: rgba(255, 255, 255, .05);
border-radius: 5px;
margin: 2px 0;
box-shadow: inset 1px 0 2px rgba(0, 0, 0, .5);
}
.lmp-window-list .lmp-tree::-webkit-scrollbar-button,
.lmp-window-option .lmp-tree::-webkit-scrollbar-button {
display: none;
width: 0;
height: 0;
}
.lmp-window-list .lmp-tree::-webkit-scrollbar-thumb,
.lmp-window-option .lmp-tree::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, #7a7a7a, #4a4a4a);
background-clip: padding-box;
border: 2px solid transparent;
border-radius: 5px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .4);
}
.lmp-window-list .lmp-tree::-webkit-scrollbar-thumb:hover,
.lmp-window-option .lmp-tree::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, #9a9a9a, #606060);
background-clip: padding-box;
}
.lmp-window-list .lmp-tree::-webkit-scrollbar-thumb:active,
.lmp-window-option .lmp-tree::-webkit-scrollbar-thumb:active,
.lmp-window-list .lmp-tree::-webkit-scrollbar-thumb:window-inactive,
.lmp-window-option .lmp-tree::-webkit-scrollbar-thumb:window-inactive {
background: linear-gradient(180deg, #ffb04a, #f57c00);
background-clip: padding-box;
}
.lmp-window-list .lmp-tree::-webkit-scrollbar-corner,
.lmp-window-option .lmp-tree::-webkit-scrollbar-corner {
background: transparent;
}
.lmp-window-list .lmp-search,
.lmp-window-option .lmp-search {
color: #888;
}
皮肤源码 · skin.js
270 行钩子脚本,导出 LMP34.skinHooks 给内核的 hooks.js 自动注册。核心职责三项:(1) 把 skin.xml 的 100P 流式坐标用 innerWidth/innerHeight 换算成像素(内核会把 100P 错读成 100px,皮肤必须自救);(2) 修正 JW 雪碧图的悬停帧切换逻辑(原雪碧图四帧 [常态,悬停,激活,激活悬停],播放中/静音时内核固定走帧 1 会显示错误图标);(3) 监听 media_time_update 钩子实时绘制进度条已播放填充层。
// JW Player 皮肤 (HTML 重制版) 专属脚本
// 原版为 CenFun CMP4 迷你皮肤:console.swf / media.swf 背景已改由 skin.css 绘制
// 钩子实现与其它 CMP4 系皮肤保持统一:
// buildLrcText / buildVideo / loadLrc / renderLrcLines / updateLrc / onLayout / onWindowToggle
// 额外职责:
// 1) 100P 流式坐标撑满视口(内核把 100P 读成 100px,必须皮肤自救)
// 2) 雪碧图悬停帧修正(JW 雪碧图 = [常态,悬停,激活,激活悬停],播放中/静音时内核固定悬停走帧 1 会显示错误图标)
var LMP34 = LMP34 || {};
LMP34.skinHooks = (function () {
// ---------- 歌词文本容器(与 tudou 实现一致) ----------
function buildLrcText(child, parentEl, winName, elements) {
var a = child.attrs;
if (child.tag === 'kmc') {
elements.lrcKmc = a;
return null;
}
var txt = document.createElement('div');
txt.className = 'lmp-lrc-text';
txt.style.position = 'absolute';
txt.style.overflow = 'hidden';
txt.style.textAlign = a.align || 'center';
txt.style.fontSize = (a.size || 12) + 'px';
if (a.font) txt.style.fontFamily = a.font;
if (a.color) {
var colors = a.color.split(',');
txt.style.color = colors[0].trim();
txt.dataset.activeColor = (colors[1] || '#fff').trim();
}
var leading = parseFloat(a.leading);
if (!isNaN(leading) && leading > 0) {
txt.style.lineHeight = leading + 'px';
}
parentEl.appendChild(txt);
elements.lrcText = txt;
return txt;
}
// ---------- 视频容器:封面图 + 频谱画布(画布交由 waveform 插件绘制) ----------
function buildVideo(child, parentEl, winName, elements) {
var video = document.createElement('div');
video.className = 'lmp-video-container';
video.style.overflow = 'hidden';
video.style.position = 'relative';
parentEl.appendChild(video);
elements.videoContainer = video;
var cover = document.createElement('img');
cover.className = 'lmp-cover';
cover.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;object-fit:contain;display:none;z-index:2;';
video.appendChild(cover);
elements.coverImg = cover;
var spectrumCanvas = document.createElement('canvas');
spectrumCanvas.className = 'lmp-spectrum';
spectrumCanvas.style.cssText = 'position:absolute;pointer-events:none;z-index:3;';
var spectrumXYWH = '0, 55P, 100P, 45P';
if (child.children) {
for (var i = 0; i < child.children.length; i++) {
if (child.children[i].tag === 'spectrum' && child.children[i].attrs.xywh) {
spectrumXYWH = child.children[i].attrs.xywh;
break;
}
}
}
spectrumCanvas.dataset.xywh = spectrumXYWH;
video.appendChild(spectrumCanvas);
elements.spectrumCanvas = spectrumCanvas;
return video;
}
// ---------- 加载 LRC ----------
function loadLrc(lrcSrc, elements, currentItem) {
if (!elements || !elements.lrcText) return;
if (elements.lrcText.dataset.loaded === '1') return;
elements.lrcText.innerHTML = '';
function showTitleFallback() {
var name = '';
if (currentItem) name = currentItem.title || currentItem.label || '';
if (name) {
var line = document.createElement('div');
line.className = 'lmp-lrc-line lmp-active';
line.textContent = name;
line.style.color = elements.lrcText.dataset.activeColor || '#ff9900';
elements.lrcText.appendChild(line);
}
}
if (!lrcSrc) { showTitleFallback(); return; }
var lrcUrl = LMP34.skinKit.proxyUrl(lrcSrc);
var xhr = new XMLHttpRequest();
xhr.open('GET', lrcUrl, true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200 || xhr.status === 0) {
var lines = LMP34.skinKit.parseLrc(xhr.responseText);
if (lines && lines.length > 0) {
LMP34.skinKit.doAction(LMP34.skinKit.NAMES.LRC_RENDER_LINES, lines, elements);
elements.lrcText.dataset.loaded = '1';
} else { showTitleFallback(); }
} else { showTitleFallback(); }
}
};
xhr.onerror = showTitleFallback;
xhr.send();
}
// ---------- 渲染歌词行 ----------
function renderLrcLines(lines, elements) {
if (!elements || !elements.lrcText) return;
elements.lrcText.innerHTML = '';
for (var i = 0; i < lines.length; i++) {
var line = document.createElement('div');
line.className = 'lmp-lrc-line';
line.textContent = lines[i].text;
line.dataset.time = lines[i].time;
elements.lrcText.appendChild(line);
}
elements.lrcText.scrollTop = 0;
}
// ---------- 逐帧高亮 ----------
function updateLrc(ct, elements) {
if (!elements || !elements.lrcText) return;
var lines = elements.lrcText.querySelectorAll('.lmp-lrc-line');
if (!lines.length) return;
var activeIdx = -1;
for (var i = lines.length - 1; i >= 0; i--) {
var t = parseFloat(lines[i].dataset.time) || 0;
if (ct >= t) { activeIdx = i; break; }
}
var activeColor = elements.lrcText.dataset.activeColor || '#ff9900';
for (var j = 0; j < lines.length; j++) {
if (j === activeIdx) {
lines[j].classList.add('lmp-active');
lines[j].style.color = activeColor;
} else {
lines[j].classList.remove('lmp-active');
lines[j].style.color = '';
}
}
if (activeIdx >= 0) {
var activeEl = lines[activeIdx];
var box = elements.lrcText;
box.scrollTop = activeEl.offsetTop - box.clientHeight / 2 + activeEl.offsetHeight / 2;
}
}
// ---------- 流式布局:撑满视口并重排所有窗口 ----------
function applyFullscreenLayout(ctx) {
if (!ctx || !ctx.container || !ctx.windows) return;
var container = ctx.container;
var windows = ctx.windows;
var vw = window.innerWidth;
var vh = window.innerHeight;
container.style.width = vw + 'px';
container.style.height = vh + 'px';
var zIndexMap = { media: 1, lrc: 5, console: 10, list: 20, option: 20 };
var relayout = LMP34.skinKit && LMP34.skinKit.relayout;
for (var name in windows) {
var win = windows[name];
if (!win) continue;
var xywh = win.dataset.xywh;
if (!xywh) continue;
var pos = LMP34.skinKit.parseXYWH(xywh, vw, vh);
win.style.left = pos.x + 'px';
win.style.top = pos.y + 'px';
win.style.width = pos.w + 'px';
win.style.height = pos.h + 'px';
if (zIndexMap[name]) win.style.zIndex = zIndexMap[name];
if (relayout) relayout(win);
}
}
// ---------- 雪碧图悬停帧修正 ----------
function fixSpriteHover(btn, isActive) {
if (!btn || btn._jwHoverFix) return;
btn._jwHoverFix = true;
btn.addEventListener('mouseenter', function () {
LMP34.view.skin.setBtnState(btn, isActive() ? 3 : 1);
});
btn.addEventListener('mouseleave', function () {
LMP34.view.skin.setBtnState(btn, isActive() ? 2 : 0);
});
}
function isPlaying() {
var btn = document.querySelector('.lmp-bt_play');
return !!(btn && btn.dataset.playing === '1');
}
function isMuted() {
try { return !!LMP34.skinKit.getMuted(); } catch (e) { return false; }
}
// ---------- 进度条"已播放"高亮 ----------
function ensureProgressFill(consoleEl) {
var bar = consoleEl && consoleEl.querySelector('.lmp-progress');
if (!bar || bar.querySelector('.lmp-progress-fill')) return;
var fill = document.createElement('div');
fill.className = 'lmp-progress-fill';
bar.insertBefore(fill, bar.firstChild);
if (!ensureProgressFill._hooked) {
ensureProgressFill._hooked = true;
LMP34.hooks.addAction(
LMP34.hooks.NAMES.MEDIA_TIME_UPDATE,
function (ct, dur) {
var f = document.querySelector('.lmp-progress-fill');
if (!f) return;
f.style.width = (dur > 0 ? Math.min(100, (ct / dur) * 100) : 0) + '%';
},
10,
window.LMP34_SKIN_NAME || 'skin'
);
}
}
// ---------- 布局 ----------
function onLayout(ctx) {
applyFullscreenLayout(ctx);
var consoleEl = ctx && ctx.windows && ctx.windows.console;
if (consoleEl) {
fixSpriteHover(consoleEl.querySelector('.lmp-bt_play'), isPlaying);
fixSpriteHover(consoleEl.querySelector('.lmp-bt_mute'), isMuted);
ensureProgressFill(consoleEl);
}
if (!onLayout._bound) {
onLayout._bound = true;
var timer = null;
LMP34.skinKit.onResize(function () {
if (timer) clearTimeout(timer);
timer = setTimeout(function () { applyFullscreenLayout(ctx); }, 120);
});
}
}
function onWindowToggle(name, isHidden, ctx) {
applyFullscreenLayout(ctx);
}
return {
buildLrcText: buildLrcText,
buildVideo: buildVideo,
loadLrc: loadLrc,
renderLrcLines: renderLrcLines,
updateLrc: updateLrc,
onLayout: onLayout,
onWindowToggle: onWindowToggle
};
})();
窗口坐标布局图
skin.xml 定义了 5 个窗口,采用流式百分比坐标(P = 百分比, R = 相对右侧),皮肤 JS 的 applyFullscreenLayout 钩子每次视口变化时换算成像素值,撑满整个视口。控制条固定底部 26px,媒体区从顶部占 26R(底部占 26px 控制条),歌词窗在媒体与控制条之间,列表/设置窗浮于右侧抽屉。
| 窗口 | xywh | 层级 z-index | 默认状态 |
|---|---|---|---|
| media (视频/封面/频谱) | 0, 0, 100P, 26R | 1 | 显示 |
| lrc (歌词) | 0, 26R, 100P, 80 | 5 | 显示 |
| console (控制条) | 0, 0R, 100P, 26 | 10 | 显示 |
| list (播放列表) | 30R, 0, 240, 26R | 20 | 隐藏 |
| option (设置) | 30R, 0, 240, 26R | 20 | 隐藏 |
作者信息
原作者: CenFun (cenfun@gmail.com, http://bbs.cenfun.com/) — 原作为 CMP4 迷你专用皮肤发布于 2008 年。
LMP34 适配升级:在保留原作者信息的前提下,将 Flash SWF 背景全部替换为 HTML/CSS/JS 原生实现,新增流式布局、雪碧图悬停帧修正、进度条填充绘制等功能,适配现代响应式屏幕。皮肤版本号已升级为 1.1,但原作者署名、语言表、tips 文案均保留。


暂无评论,快来抢沙发~