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,直接复制替换即可:

CMP4 JW Player 皮肤 · LMP34 适配版
<!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, 26R1显示
lrc (歌词)0, 26R, 100P, 805显示
console (控制条)0, 0R, 100P, 2610显示
list (播放列表)30R, 0, 240, 26R20隐藏
option (设置)30R, 0, 240, 26R20隐藏

作者信息

原作者: CenFun (cenfun@gmail.com, http://bbs.cenfun.com/) — 原作为 CMP4 迷你专用皮肤发布于 2008 年。
LMP34 适配升级:在保留原作者信息的前提下,将 Flash SWF 背景全部替换为 HTML/CSS/JS 原生实现,新增流式布局、雪碧图悬停帧修正、进度条填充绘制等功能,适配现代响应式屏幕。皮肤版本号已升级为 1.1,但原作者署名、语言表、tips 文案均保留。