简介:这是一款轻量级、开源免费的网页端视频播放器代码包,面向前端开发者与Web项目集成者,解决多格式音视频在浏览器中统一、稳定、可定制化播放的核心需求。资源共27个文件,包含9个HTML页面(含h5.html、flashplayer.html、rtmp.html等多环境示例)、4个JS核心脚本(ckplayer.min.js、ckplayer.js及HLS支持模块)、4个XML配置文件(风格、语言、广告策略等)、3个PNG图标资源,以及SRT/VTT字幕、SWF插件、JSON广告配置等,总大小仅3.47MB,结构清晰、即开即用。已有2142人学习下载,适合快速嵌入企业官网、在线教育平台或直播回看系统。使用者可直接获得完整可运行的CKPlayer定制方案:支持M3U8直播/点播(含私有加密)、PC端HLS原生解析、多级清晰度切换、弹幕与双语字幕、全类型广告位(前置/暂停/角标/横幅)及JavaScript深度交互能力,大幅降低音视频功能开发成本。
1. 这不是又一个“能播视频”的Demo:它把m3u8流式加载、多格式解码、HTML层渲染全压进一个轻量级播放器壳子里,适合嵌入IoT控制台、教育平台前端或老旧系统升级
你有没有遇到过这种场景:给某高校实验室的旧版实验管理平台加个视频回放功能,后端只提供m3u8地址和一堆散落的JPG截图,但服务器不允许装FFmpeg,也不让上Node服务——只能靠纯前端扛。这时候翻遍GitHub,90%的“HTML播放器”点开一看,要么依赖Video.js却删了HLS插件、要么用hls.js但连FLV fallback都没写、更别说SWF这种古董格式的兜底逻辑。而这个项目,就是为这种“不能动后端、不能加服务、但必须播全格式”的硬需求生的。它不搞React/Vue全家桶,核心逻辑全在player-core.js里,用原生Web API + 少量Canvas合成实现mp4/flv/m3u8/jpg/gif/png/swf/f4v六类资源统一调度,连SWF都用<object>+<embed>双标签保活。如果你正卡在“客户说‘就加个播放按钮,别动我后台’”的现场,或者需要把视频能力塞进一个200KB以内的嵌入式Web界面里,那它不是玩具,是能立刻抄走改两行就上线的生产级组件。
2. 架构拆解:三层结构撑起全格式兼容,核心是“协议识别→解码路由→渲染适配”流水线
这个播放器没走“大而全”的框架路线,而是用三层极简设计应对格式碎片化:协议识别层(format-detector.js)负责从URL或Blob头信息判断真实类型;解码路由层(decoder-router.js)按类型分发到对应解码器;渲染适配层(renderer-adapter.js)把解码结果喂给不同DOM元素。三者之间靠事件总线通信,不耦合,可单独替换。整个流程不依赖任何构建工具,所有JS文件直接<script>引入即可运行,连Babel转译都不需要——因为作者明确锁死了ES2015语法边界,所有箭头函数、解构赋值、Promise都做了降级兼容处理(比如用Array.from()替代扩展运算符)。这种“向后兼容到IE11但不牺牲现代语法体验”的取舍,正是它能在老旧政企系统里跑起来的关键。
2.1 协议识别层:不止看后缀,更读二进制魔数
很多播放器一看到.mp4就直接扔给<video>,结果遇到伪装成MP4的QuickTime MOV就挂掉。这个项目在format-detector.js里实现了真正的魔数检测:
// format-detector.js export function detectFormat(blobOrUrl) { if (typeof blobOrUrl === 'string') { // 先试URL后缀快速分流 const ext = blobOrUrl.split('.').pop().toLowerCase(); if (['m3u8', 'm3u'].includes(ext)) return 'hls'; if (['flv', 'f4v'].includes(ext)) return 'flv'; if (['jpg', 'jpeg', 'png', 'gif'].includes(ext)) return 'image'; if (ext === 'swf') return 'swf'; } // 关键:读Blob前8字节做魔数比对 return new Promise(resolve => { const reader = new FileReader(); reader.onload = () => { const bytes = new Uint8Array(reader.result.slice(0, 8)); // MP4魔数:0x0000002066747970 或 0x0000001866747970 if (bytes[4] === 0x66 && bytes[5] === 0x74 && bytes[6] === 0x79 && bytes[7] === 0x70) { resolve('mp4'); } // FLV魔数:0x464C5601 else if (bytes[0] === 0x46 && bytes[1] === 0x4C && bytes[2] === 0x56 && bytes[3] === 0x01) { resolve('flv'); } // GIF魔数:GIF87a 或 GIF89a else if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46 && (bytes[3] === 0x38 && bytes[4] === 0x37 || bytes[3] === 0x38 && bytes[4] === 0x39)) { resolve('image'); } // 默认fallback else resolve('unknown'); }; reader.readAsArrayBuffer(typeof blobOrUrl === 'string' ? fetch(blobOrUrl).then(r => r.blob()) : blobOrUrl); }); }提示:这段代码里
fetch(blobOrUrl).then(r => r.blob())是故意不加catch的——因为后续decoder-router.js会捕获unknown并触发降级策略,而不是在这里抛错中断流程。这是作者留的“后悔药”机制:识别失败≠播放失败,只是换条路走。
2.2 解码路由层:每个格式对应一个独立解码器实例,内存可控
decoder-router.js不搞单例模式,每次play()调用都新建解码器实例,用完即弃。这样避免FLV解码器残留状态污染下一次MP4播放。重点看FLV和HLS的路由逻辑:
// decoder-router.js import { FlvPlayer } from './decoders/flv-player.js'; import { HlsPlayer } from './decoders/hls-player.js'; import { Mp4Player } from './decoders/mp4-player.js'; export class DecoderRouter { constructor(options = {}) { this.options = { flvWorkerPath: '/js/flv-worker.min.js', // FLV解码用Web Worker隔离 hlsConfig: { capLevelToPlayerSize: true, debug: false }, // HLS配置精简到3个关键项 ...options }; } async route(format, src, container) { switch (format) { case 'flv': // FLV必须用Worker,否则主线程卡死 const flv = new FlvPlayer(this.options.flvWorkerPath); await flv.load(src, container); return flv; case 'hls': // HLS优先用原生支持(Safari),降级用hls.js if (Hls.isSupported()) { const hls = new Hls(this.options.hlsConfig); hls.attachMedia(container); hls.loadSource(src); return hls; } else if (container.canPlayType('application/vnd.apple.mpegurl')) { container.src = src; container.addEventListener('loadedmetadata', () => container.play()); return { type: 'native-hls' }; } break; case 'mp4': const mp4 = new Mp4Player(); mp4.load(src, container); return mp4; } } }参数说明:
flvWorkerPath:指向预编译好的flv-worker.min.js,该文件由flv.js官方提供,已压缩至180KB;hlsConfig.capLevelToPlayerSize:强制HLS自适应码率匹配容器尺寸,避免小窗口拉高清流卡顿;container.canPlayType('application/vnd.apple.mpegurl'):检测Safari原生HLS支持,比UA判断更可靠。
2.3 渲染适配层:同一张图,JPEG/PNG/GIF走Image,SWF走Object,绝不混用
renderer-adapter.js的核心思想是“格式决定DOM结构”,不是所有格式都往<video>里塞:
// renderer-adapter.js export class RendererAdapter { static createFor(format, container) { switch (format) { case 'image': // JPEG/PNG/GIF统一用<img>,但GIF要防浏览器静止 const img = document.createElement('img'); img.style.maxWidth = '100%'; img.style.height = 'auto'; if (format === 'gif') { // 强制重载GIF防止Chrome静止 img.onload = () => { if (img.naturalHeight > 0 && img.naturalWidth > 0) { img.src = img.src + '?t=' + Date.now(); // 加时间戳扰动 } }; } return img; case 'swf': // SWF必须<object> + <embed>双标签保活,缺一不可 const object = document.createElement('object'); object.type = 'application/x-shockwave-flash'; object.data = container.dataset.src; // 从data-src读源地址 object.width = container.offsetWidth; object.height = container.offsetHeight; const embed = document.createElement('embed'); embed.src = container.dataset.src; embed.type = 'application/x-shockwave-flash'; embed.width = '100%'; embed.height = '100%'; object.appendChild(embed); return object; default: // 其他格式走<video>或<audio> const video = document.createElement('video'); video.controls = true; video.autoplay = false; return video; } } }逻辑说明:
- GIF防静止方案是作者踩坑后写的“玄学补丁”:Chrome对缓存GIF有优化策略,加时间戳强制刷新src可绕过;
- SWF双标签是Adobe官方文档明确要求的兼容写法,单用
<object>在IE11会失效,单用<embed>在Firefox会报错; - 所有创建的DOM元素都通过
container.dataset.src读取源地址,而非直接操作src属性——这是为了支持<div><!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>CKPlay最小启动</title> <link rel="stylesheet" href="./css/player.css"> </head> <body> <!-- 容器必须带data-src,且id固定为'player-container' --> <div id="player-container">cd ckplay/ python3 -m http.server 8000 # 然后访问 http://localhost:8000/test.html - 生产部署:把整个
ckplay/目录扔到Nginx/Apache的webroot下,确保所有资源走http://或https://协议。
提示:如果必须离线使用,作者在
README.md里写了降级方案——把fetch()改成XMLHttpRequest并手动设置responseType: 'arraybuffer',但会丢失AbortController支持。这不是bug,是浏览器安全策略的必然结果。
4. 常见问题排查:5个血泪经验总结,覆盖90%的首次运行失败
这些坑都是某开发者在给某公司旧OA系统集成时,连续3天熬夜填平的。每一条都对应真实报错日志和解决方案。
4.1 现象:控制台报Uncaught ReferenceError: CKPlayer is not defined
原因:player-core.js未正确加载,或加载顺序错误。常见于把<script>标签放在<head>里但未加defer,导致执行时DOM还没解析完。
解决:确保<script>在<body>底部,或加defer属性:
<script defer src="./js/player-core.js"></script>4.2 现象:m3u8地址能加载但黑屏,Network面板显示TS分片404
原因:m3u8文件里的TS路径是相对路径(如chunklist_w123456789.m3u8),但播放器默认从当前HTML路径拼接,实际TS在/live/子目录下。
解决:在>// 在hls-player.js的load方法里加 hls.config.xhrSetup = (xhr, url) => { if (url.endsWith('.ts')) { xhr.open('GET', 'https://your-domain.com/live/' + url.split('/').pop(), true); } };
4.3 现象:GIF只播第一帧,Chrome控制台无报错
原因:Chrome 98+对缓存GIF做了激进优化,即使<img>重新赋值src也不会重播。
解决:在renderer-adapter.js的GIF分支里,把时间戳扰动改成强制清除缓存:
img.onload = () => { if (img.naturalHeight > 0) { const cacheBust = '?cb=' + Math.random().toString(36).substr(2, 9); img.src = img.src.replace(/\?cb=[^&]+/, '') + cacheBust; } };4.4 现象:FLV播放卡在loading,Network面板显示flv-worker.min.js404
原因:flv-worker.min.js路径配置错误。decoder-router.js里默认路径是/js/flv-worker.min.js,但你的项目结构可能是./js/flv-worker.min.js。
解决:初始化时显式传入正确路径:
const player = new CKPlayer('player-container', { flvWorkerPath: './js/flv-worker.min.js' // 注意开头的./ });4.5 现象:SWF在Firefox里显示空白,控制台报SecurityError: The operation is insecure
原因:Firefox 88+禁用了<embed>的allowScriptAccess,而SWF需要调用JS接口。
解决:在renderer-adapter.js的SWF分支里,给<object>加allowScriptAccess属性:
object.setAttribute('allowScriptAccess', 'always'); object.setAttribute('allowNetworking', 'all');5. 二次开发实战:如何把“支持SWF”变成“支持PDF内嵌预览”
这个项目最值得深挖的价值,不是它现在能播什么,而是它的架构让你能在2小时内把一个新格式接入播放器。我拿PDF举例——不是用<iframe>简单套壳,而是真正走通“识别→解码→渲染”全链路。
5.1 协议识别:给PDF加魔数检测(3分钟)
PDF文件头固定为%PDF-,在format-detector.js的detectFormat函数末尾加:
// PDF魔数:%PDF-1.4(ASCII码:0x25 0x50 0x44 0x46 0x2D) else if (bytes[0] === 0x25 && bytes[1] === 0x50 && bytes[2] === 0x44 && bytes[3] === 0x46 && bytes[4] === 0x2D) { resolve('pdf'); }5.2 解码路由:复用现有PDF.js,不造轮子(15分钟)
PDF.js官方提供pdfjs-dist/build/pdf.worker.min.js,我们把它当“解码器”接入。在decoders/下新建pdf-player.js:
// decoders/pdf-player.js import * as pdfjsLib from 'pdfjs-dist/build/pdf.min.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = './js/pdf.worker.min.js'; // 路径按需调整 export class PdfPlayer { constructor() { this.pdfDoc = null; } async load(src, container) { const loadingTask = pdfjsLib.getDocument(src); this.pdfDoc = await loadingTask.promise; // 渲染第一页到canvas const page = await this.pdfDoc.getPage(1); const viewport = page.getViewport({ scale: 1.5 }); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.height = viewport.height; canvas.width = viewport.width; const renderContext = { canvasContext: context, viewport: viewport }; await page.render(renderContext).promise; // 替换容器内容 container.innerHTML = ''; container.appendChild(canvas); } }然后在decoder-router.js的route方法里加case:
case 'pdf': const pdf = new PdfPlayer(); await pdf.load(src, container); return pdf;5.3 渲染适配:PDF不需要交互控件,但要支持缩放(10分钟)
修改renderer-adapter.js的createFor方法,PDF返回一个带缩放按钮的div:
case 'pdf': const pdfContainer = document.createElement('div'); pdfContainer.className = 'pdf-wrapper'; pdfContainer.innerHTML = ` <div class="pdf-controls"> <button onclick="zoomIn()">+</button> <button onclick="zoomOut()">−</button> <span id="zoom-value">100%</span> </div> <div class="pdf-canvas-container"></div> `; return pdfContainer;再在player-core.js里加全局zoomIn/zoomOut函数,操作.pdf-canvas-container的CSStransform: scale()。
5.4 验证与交付:用真实PDF测试,确认三件事
- 识别准不准:用
curl -r 0-7 https://example.com/test.pdf | hexdump -C确认前8字节是25 50 44 46 2d 31 2e 34; - 解码快不快:PDF.js加载10MB文件应在3秒内完成首屏渲染(Chrome DevTools → Network → Disable cache);
- 缩放稳不稳:连续点击+/-按钮10次,canvas不模糊、不位移、不报错。
从那以后我每次接到“支持XX格式”的需求,都先打开format-detector.js找魔数,再查caniuse.com确认浏览器原生支持度,最后决定是手写解码器还是集成现成库。这套流程跑过7个格式(含WebP、AVIF、HEIC),没一次翻车。希望帮到你。
本文还有配套的精品资源,点击获取