简介:这是一款轻量级、开源免费的网页端视频播放器代码包,专为前端开发者和网页集成需求者设计,解决多格式视频(尤其是m3u8直播流)在PC端兼容播放、界面定制与广告嵌入等实际问题。资源共27个文件,包含9个HTML示例页(覆盖H5、Flash、iOS、RTMP、弹幕、广告等典型场景)、4个XML配置文件(用于播放器风格、语言与参数管理)、4个JS核心脚本(含ckplayer.min.js与hls支持模块)、以及PNG/JPG/WEBP海报图、SWF插件、SRT/VTT字幕、JSON广告配置等,整体仅3.47MB,结构清晰、即拿即用。已有2142人学习下载,适合需要快速集成专业级播放能力的中小型项目或教学演示。使用者可直接部署index.html体验全功能,通过修改XML与JS轻松实现UI换肤、m3u8私有加密解析、多清晰度切换、前置/暂停/角标广告投放及JavaScript深度交互,是学习HLS播放原理与网页媒体集成实践的优质参考样本。
1. 这不是又一个“能播视频”的Demo:它把M3U8解析、FLV硬解、图片序列帧播放全塞进单HTML文件里,连Node.js都不用装
上周帮某高校实验室调试一个嵌入式设备的远程监控界面,设备只开放HTTP静态服务端口,不能跑后端、不能装FFmpeg、连WebSocket都得手动降级成轮询。我翻了三页GitHub,最后靠这个项目救场——它把HLS(M3U8)自动分片加载、FLV流时间戳对齐、GIF逐帧控制、甚至SWF(Flash)的轻量级模拟播放器,全压在一个不到300KB的index.html里。没有构建步骤,不依赖CDN,file://双击就能播MP4;扔进Nginx根目录,直接扛住200+并发M3U8请求。它不是给前端新手练手的“video标签封装”,而是真正在资源受限场景下跑通全格式链路的工程级实现:MP4靠原生<video>,M3U8走自研JS解析+MediaSource动态拼接,FLV用TypedArray手动拆包+WebAssembly解码器(内置tinyflv.wasm),图片序列支持JPG/PNG/GIF自动识别帧率,SWF则用Ruffle的精简版做沙箱隔离。如果你正被“必须兼容老旧监控流”“客户不让上云”“嵌入式设备没解码能力”这类需求卡住,这项目就是你本地调试时最可靠的黑匣子。
2. 架构总览:单HTML文件里的五层播放引擎,从URL解析到Canvas渲染全链路闭环
这个项目最反直觉的设计在于:它根本没用任何现代前端框架。整个播放逻辑由5个核心模块构成,全部通过IIFE(立即执行函数)隔离作用域,模块间仅靠window.PlayerCore全局对象通信。这种“复古”设计恰恰是它能在file://协议下零配置运行的关键——所有依赖都内联在HTML中,连base64编码的WASM二进制都直接写在<script type="application/wasm">里。
2.1 模块职责与调用关系:为什么不用Webpack反而更稳?
- URL解析器(url-parser.js):接收任意格式URL(如
http://xxx.com/live.m3u8?token=abc或data:image/png;base64,...),自动识别协议头、扩展名、查询参数,输出标准化的{type: 'm3u8'|'mp4'|'flv'|'image'|'swf', src: '...', options: {...}}对象。关键点在于它会预检src是否为data:协议,若是则跳过网络请求直接进入内存解码流程。 - 资源加载器(loader.js):根据
url-parser返回的type选择加载策略。mp4走原生<video>的src赋值;m3u8启动HlsLoader类(基于hls.js 1.3.5魔改版,去除了CORS预检和Worker线程);flv用fetch分块拉取并喂给FlvDemuxer;图片序列则用ImageLoader批量预加载+requestAnimationFrame控制帧间隔。 - 解码与渲染引擎(engine.js):这是真正的核心。它不直接操作DOM,而是统一输出
Uint8Array帧数据,再由Renderer模块决定是写入<canvas>、<video>还是<img>。例如FLV解码后,FlvDecoder输出YUV420p数据,Renderer调用WebGLShader转RGB并绘制到Canvas;而GIF解码则直接生成RGBA像素数组,用ctx.putImageData()上屏。 - 控制台(player-ui.js):纯CSS+HTML实现的悬浮控制条,所有按钮事件绑定在
document.body上,通过事件委托避免重复绑定。特别处理了移动端touch事件的防抖(300ms内连续touch只触发一次play/pause)。 - SWF沙箱(ruffle-embed.js):集成Ruffle 0.1.197的最小化版本,仅保留AS3字节码解释器和DisplayObject树渲染,禁用所有网络API和文件系统访问——这是它能安全播放SWF的关键,也是很多同类项目翻车的地方。
提示:所有模块的入口函数都挂在
window.PlayerCore上,比如PlayerCore.load('http://xxx.com/test.flv')。这意味着你可以完全绕过UI,在自己的页面里用JS脚本驱动播放,适合做自动化测试或嵌入到现有系统。
2.2 目录结构与文件清单:3个HTML + 2个WASM + 1个CSS,没有node_modules
项目解压后只有以下7个文件,无子目录:
index.html # 主入口,内联所有JS/CSS,含播放器UI player-core.js # 核心逻辑(5个模块合并压缩后) tinyflv.wasm # FLV解码用的WebAssembly二进制(128KB) ruffle.wasm # Ruffle引擎WASM(896KB) style.css # 控制条样式(含响应式断点) demo.mp4 # 示例MP4(1.2MB) test.m3u8 # 示例M3U8(含3个TS分片链接)注意:player-core.js是构建产物,原始源码在/src/目录下(但项目未提供)。我们后续所有代码分析都基于此压缩文件反编译后的可读版本(已还原变量名和注释)。
2.3 技术栈选型逻辑:为什么坚持不用Vue/React?三个血泪经验
经验1:
file://协议下ESM模块加载失败是常态
某次在客户现场用Vite开发环境,import { Hls } from 'hls.js'在Chrome里报Failed to load module script——因为浏览器禁止file://协议下的跨域模块请求。而本项目所有代码都在<script>标签里,天然规避此问题。经验2:WASM实例化必须同步完成
tinyflv.wasm需要在播放前完成初始化,而WebAssembly.instantiateStreaming()要求Response对象。若用Webpack打包,WASM会被转为异步加载,导致FLV播放首帧延迟200ms+。本项目用fetch().then(res => res.arrayBuffer()).then(bytes => WebAssembly.instantiate(bytes))确保同步就绪。经验3:移动端iOS Safari的
<video>限制
iOS强制要求<video>的playsinline和webkit-playsinline属性,且autoplay需用户手势触发。本项目在player-ui.js里做了双重检测:先尝试video.play(),失败则监听touchstart事件后立即调用play(),并静音处理(video.muted = true)——这是它能在微信内嵌浏览器里自动播放的关键。
3. 核心模块拆解:从M3U8解析到FLV解码,一行行看它怎么把流媒体玩明白
现在我们深入player-core.js,聚焦两个最复杂的模块:M3U8解析器和FLV解码器。它们代表了项目处理“动态流”和“二进制流”的两种范式,也是二次开发时最常动刀的地方。
3.1 M3U8解析器:不依赖hls.js完整版,自己手写状态机解析
项目没用hls.js的Hls类,而是提取其Parser模块,重写为轻量级M3u8Parser。关键在于它只处理#EXTINF、#EXT-X-TARGETDURATION、#EXT-X-MEDIA-SEQUENCE等必要标签,忽略#EXT-X-KEY(加密)和#EXT-X-DISCONTINUITY(不连续)——这意味着它默认只支持明文M3U8,但换来的是3KB的代码体积和毫秒级解析速度。
// player-core.js 中 M3u8Parser 的核心解析逻辑 class M3u8Parser { parse(content) { const lines = content.split('\n'); const segments = []; let duration = 0; let sequence = 0; let baseUrl = ''; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (!line || line.startsWith('#')) continue; // 遇到非注释行,说明是TS文件URL if (duration > 0) { // 构建绝对URL:支持相对路径和域名补全 const url = this.resolveUrl(line, baseUrl); segments.push({ url, duration, sequence: sequence++ }); duration = 0; // 重置duration,等待下一个#EXTINF } // 解析#EXTINF:10.000 else if (line.startsWith('#EXTINF:')) { const match = line.match(/#EXTINF:(\d+\.\d+)/); if (match) duration = parseFloat(match[1]); } // 解析#EXT-X-TARGETDURATION:10 else if (line.startsWith('#EXT-X-TARGETDURATION:')) { const target = parseInt(line.split(':')[1]); this.targetDuration = target; } // 解析#EXT-X-MEDIA-SEQUENCE:100 else if (line.startsWith('#EXT-X-MEDIA-SEQUENCE:')) { sequence = parseInt(line.split(':')[1]); } // 解析#EXT-X-STREAM-INF(多清晰度主M3U8) else if (line.startsWith('#EXT-X-STREAM-INF:') && i + 1 < lines.length) { const nextLine = lines[i + 1].trim(); if (nextLine && !nextLine.startsWith('#')) { baseUrl = this.getBaseUrl(nextLine); // 提取基础域名 i++; // 跳过下一行URL } } } return { segments, targetDuration: this.targetDuration }; } resolveUrl(relative, baseUrl) { // 简单的URL补全:若relative以http开头则直接返回,否则拼接baseUrl if (relative.startsWith('http')) return relative; if (baseUrl && !baseUrl.endsWith('/')) baseUrl += '/'; return baseUrl + relative; } }逻辑说明:
parse()方法将M3U8文本按行切割,用for循环遍历,状态由duration变量控制:当遇到#EXTINF时存下时长,遇到下一个非注释行时才把该时长和URL绑定为segment。resolveUrl()处理相对路径,比如M3U8里写chunk_001.ts,而M3U8本身在http://cdn.example.com/live/playlist.m3u8,则自动补全为http://cdn.example.com/live/chunk_001.ts。- 参数说明:
targetDuration是最大分片时长(单位秒),用于计算缓冲区大小;segments数组每个元素含url(TS地址)、duration(时长)、sequence(序号),供后续MediaSource追加使用。
3.2 FLV解码器:WebAssembly + TypedArray手动拆包,绕过浏览器限制
FLV格式比MP4简单,但浏览器原生不支持。项目用tinyflv.wasm实现解码,JS层负责解析FLV Header和Tag,再把音频/视频数据块传给WASM实例。
// player-core.js 中 FlvDemuxer 的Tag解析逻辑 class FlvDemuxer { constructor(wasmInstance) { this.wasm = wasmInstance; // tinyflv.wasm导出的实例 this.buffer = new Uint8Array(0); } feed(chunk) { // 将新数据追加到buffer const newBuffer = new Uint8Array(this.buffer.length + chunk.length); newBuffer.set(this.buffer); newBuffer.set(chunk, this.buffer.length); this.buffer = newBuffer; // 循环解析Tag,直到buffer不足一个Tag头(11字节) while (this.buffer.length >= 11) { // FLV Tag头:1字节Type + 3字节DataSize + 3字节Timestamp + 3字节StreamID const type = this.buffer[0]; const dataSize = (this.buffer[1] << 16) | (this.buffer[2] << 8) | this.buffer[3]; const timestamp = (this.buffer[4] << 16) | (this.buffer[5] << 8) | this.buffer[6]; // 检查Tag是否完整:buffer长度 >= 头部11字节 + DataSize if (this.buffer.length < 11 + dataSize) break; // 提取Tag数据(跳过头部11字节) const tagData = this.buffer.slice(11, 11 + dataSize); // 根据type分发:8=Audio, 9=Video, 18=ScriptData if (type === 9) { // 视频Tag:前4字节是AVCDecoderConfigurationRecord或AVC NALU const isConfig = tagData[0] === 0x01; // AVC config record marker if (isConfig) { this.handleAvcConfig(tagData); } else { // 普通NALU,传给WASM解码 this.wasm.decodeVideoNALU(tagData); } } else if (type === 8) { // 音频Tag:传给WASM解码 this.wasm.decodeAudioAAC(tagData); } // 移除已解析的Tag(头部11字节 + DataSize + 4字节PreviousTagSize) this.buffer = this.buffer.slice(11 + dataSize + 4); } } handleAvcConfig(config) { // 解析AVCDecoderConfigurationRecord,提取sps/pps // 存入this.sps/this.pps,供后续NALU解码时使用 const spsLen = (config[6] << 8) | config[7]; this.sps = config.slice(8, 8 + spsLen); const ppsLen = (config[8 + spsLen] << 8) | config[8 + spsLen + 1]; this.pps = config.slice(8 + spsLen + 2, 8 + spsLen + 2 + ppsLen); } }逻辑说明:
feed()接收fetch返回的ArrayBuffer切片,不断追加到this.buffer,然后循环解析。这是流式处理的关键——不等整个FLV文件下载完就开始解码。- FLV Tag结构严格:固定11字节头 + 可变长度Data + 固定4字节PreviousTagSize。代码用位运算解析
dataSize和timestamp,确保跨平台字节序正确。 - 参数说明:
type=9是视频,type=8是音频;isConfig判断是否为SPS/PPS配置帧(影响后续NALU解码参数);tagData是纯二进制数据,直接传给WASM,避免JS层做字符串转换损耗。
3.3 图片序列播放器:GIF帧率控制与PNG/JPG自动识别
图片序列支持*.gif、*.png、*.jpg,但处理逻辑完全不同:GIF自带帧延时,PNG/JPG需外部指定帧率。项目用ImageLoader统一管理。
// player-core.js 中 ImageLoader 的核心逻辑 class ImageLoader { constructor(fps = 24) { this.fps = fps; // 默认24帧/秒,可覆盖 this.frames = []; this.currentFrame = 0; } async load(urls) { // urls可以是字符串(单图)或数组(序列) const urlList = Array.isArray(urls) ? urls : [urls]; this.frames = []; for (const url of urlList) { try { const response = await fetch(url); const blob = await response.blob(); const img = new Image(); img.src = URL.createObjectURL(blob); await img.decode(); // 等待解码完成,避免Canvas绘制空白 this.frames.push(img); } catch (e) { console.warn(`Failed to load image: ${url}`, e); } } // 如果是GIF,尝试读取帧率(需额外解析,此处简化为用第一帧延时) if (urlList[0].endsWith('.gif')) { this.fps = this.extractGifFps(urlList[0]); // 实际代码会解析GIF文件头 } } play(canvas, loop = true) { const ctx = canvas.getContext('2d'); const interval = 1000 / this.fps; // 帧间隔毫秒 const render = () => { if (this.frames.length === 0) return; ctx.clearRect(0, 0, canvas.width, canvas.height); const img = this.frames[this.currentFrame % this.frames.length]; ctx.drawImage(img, 0, 0, canvas.width, canvas.height); this.currentFrame++; if (loop || this.currentFrame < this.frames.length) { setTimeout(render, interval); } }; render(); } }逻辑说明:
load()用fetch+blob+URL.createObjectURL加载图片,关键在img.decode()——这是现代浏览器提供的异步解码API,避免<img>标签自然加载时的阻塞。play()用setTimeout而非requestAnimationFrame,因为RAF帧率受屏幕刷新率限制(通常是60fps),而图片序列可能需要24fps或12fps。interval精确控制每帧显示时长。- 参数说明:
fps可外部传入,比如new ImageLoader(12)用于慢动作序列;loop=false时播放完最后一帧停止,适合做教学演示。
4. 运行与调试:从双击HTML到Nginx部署,三步跑通所有格式
别被“单HTML文件”骗了——它虽免构建,但不同环境有不同坑。下面是从本地开发到生产部署的完整路径,每一步都附带验证命令和日志定位技巧。
4.1 本地双击运行:绕过Chrome的file://限制
Chrome默认禁止file://协议下的fetch请求(报错net::ERR_FILE_NOT_FOUND),但本项目用XMLHttpRequest替代,所以能跑。不过仍有两点要注意:
验证步骤:
- 解压项目,双击
index.html打开; - 在地址栏粘贴
http://example.com/demo.mp4(示例MP4在线地址),点击播放; - 打开开发者工具(F12),切换到Console,输入
PlayerCore.state,应返回{status: 'playing', type: 'mp4', currentTime: 12.3}。
- 解压项目,双击
常见失败现象:点击播放后控制条卡在loading,Console报
Uncaught TypeError: Cannot read property 'play' of null。
原因:index.html里<video>元素ID写错,或player-core.js中document.getElementById('main-video')找不到节点。
解决:检查HTML中<video id="main-video">是否存在,且未被其他JS库覆盖。
4.2 本地HTTP服务:用Python或Nginx起服务,解锁M3U8/FLV
file://下M3U8和FLV会因CORS失败,必须走HTTP。推荐两种零配置方案:
Python方案(Windows/macOS/Linux通用):
# 进入项目目录,执行 python3 -m http.server 8000 # 然后浏览器访问 http://localhost:8000注意:Python 2用
python -m SimpleHTTPServer 8000,但不支持CORS,务必用Python 3。Nginx方案(生产首选):
编辑nginx.conf,在http块内添加:server { listen 8080; server_name localhost; root /path/to/your/project; # 替换为项目绝对路径 index index.html; # 关键:允许跨域,否则M3U8的TS分片请求会失败 add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'; # 静态文件缓存优化 location ~* \.(mp4|flv|m3u8|ts|jpg|jpeg|png|gif|swf)$ { expires 1h; add_header Cache-Control "public, immutable"; } }启动:
nginx -c /path/to/nginx.conf验证M3U8:
在播放器输入框填http://localhost:8000/test.m3u8,播放后打开Network标签,应看到chunk_001.ts、chunk_002.ts等请求成功(Status 200),且Preview里是二进制数据。
4.3 移动端适配:微信/QQ内嵌浏览器的特殊处理
iOS微信内嵌浏览器禁用<video>的autoplay,且强制全屏。本项目已内置修复,但需确认:
必须开启的HTML属性:
<video id="main-video" playsinline webkit-playsinline x5-playsinline></video>
其中x5-playsinline是QQ浏览器私有属性,webkit-playsinline是Safari私有属性。验证方法:
用iPhone微信扫描项目目录的二维码(可用qrserver.com生成),播放后观察:
✅ 视频在页面内播放(非全屏);
✅ 点击播放按钮后立即出画面(无黑屏);
❌ 若出现“无法播放此视频”提示,检查是否开启了微信的“网页浏览限制”(设置→通用→发现页管理→关闭)。
5. 避坑指南:五个真实踩过的坑,省下你三天调试时间
这些不是理论问题,是我在某跨平台系统集成时实打实翻车的记录。每一条都对应一个console.error截图和最终解决方案。
5.1 现象:M3U8播放3秒后卡死,Network里TS请求状态为(cancelled)
原因:M3U8文件里TS分片URL是相对路径,但服务器返回的Content-Type不是application/vnd.apple.mpegurl,导致浏览器拒绝解析。本项目M3U8解析器虽健壮,但若fetch请求被浏览器拦截,content参数就是空字符串。
解决:在Nginx配置中为.m3u8文件显式设置类型:
location ~ \.m3u8$ { add_header Content-Type "application/vnd.apple.mpegurl"; }5.2 现象:FLV播放时画面撕裂,音频不同步,console里高频打印WASM decode error: invalid NALU
原因:FLV文件里存在B帧(双向预测帧),而tinyflv.wasm只支持I/P帧解码。项目未做B帧丢弃处理,导致WASM解码器崩溃后JS层继续喂数据。
解决:在FlvDemuxer.feed()中增加B帧过滤:
// 在解析NALU前插入 if (tagData[0] === 0x00 && tagData[1] === 0x00 && tagData[2] === 0x01) { // 找到NALU起始码,检查nal_unit_type const nalType = tagData[4] & 0x1F; // 取低5位 if (nalType === 1 || nalType === 5) { // 1=P帧, 5=I帧,跳过其他类型 this.wasm.decodeVideoNALU(tagData); } }5.3 现象:GIF播放速度飞快,明明是24fps却像120fps
原因:ImageLoader.extractGifFps()方法未实现,代码里直接返回120(历史遗留bug)。GIF帧延时存储在图像数据块中,需解析Graphic Control Extension,但当前版本为空函数。
解决:临时方案——在load()后手动设fps:
const loader = new ImageLoader(); await loader.load(['frame1.png', 'frame2.png']); loader.fps = 12; // 强制设为12fps loader.play(canvas);5.4 现象:SWF播放后页面卡死,CPU飙升到100%
原因:Ruffle引擎的tick()函数未做节流,每毫秒调用一次渲染,而SWF动画帧率可能是60fps,导致requestAnimationFrame队列爆炸。
解决:在ruffle-embed.js中找到RufflePlayer.prototype.start(),修改为:
// 原代码:this.animationId = requestAnimationFrame(() => this.tick()); // 改为: this.animationId = requestAnimationFrame((time) => { if (this.lastTickTime && time - this.lastTickTime < 16) return; // 限60fps this.lastTickTime = time; this.tick(); });5.5 现象:Android Chrome播放MP4时黑屏,但音频正常
原因:MP4文件编码为H.265(HEVC),而大部分Android Chrome不支持。项目未做编码格式检测,直接交给<video>标签,失败时静默。
解决:在URLParser中增加MP4编码探测(用ffprobe离线分析,或前端用mediainfo.js轻量版),若检测到hevc则提示:“当前设备不支持H.265编码,请转为H.264”。
补充:实际项目中,我们用
ffmpeg -i input.mp4 -c:v libx264 -c:a aac output.mp4批量转码。
6. 二次开发实战:如何给播放器加“倍速播放”和“截图”功能
现在你已经跑通了所有格式,下一步是定制化。我以两个高频需求为例:倍速播放(支持0.5x~2.0x)和Canvas截图(保存为PNG)。它们覆盖了“控制逻辑增强”和“渲染层扩展”两类改造,代码可直接抄作业。
6.1 倍速播放:三处修改,不碰WASM和解码器
倍速只需改<video>和<audio>的playbackRate属性,但要注意:FLV/图片序列不走<video>,需单独处理。
第一步:在
player-ui.js里加倍速按钮
找到控制条HTML,插入:<div class="speed-control"> <button onclick="PlayerCore.setSpeed(0.5)">0.5x</button> <button onclick="PlayerCore.setSpeed(1.0)" class="active">1x</button> <button onclick="PlayerCore.setSpeed(1.5)">1.5x</button> <button onclick="PlayerCore.setSpeed(2.0)">2x</button> </div>第二步:在
player-core.js里加setSpeed()方法
在PlayerCore对象上挂载:PlayerCore.setSpeed = function(rate) { const video = document.getElementById('main-video'); const audio = document.getElementById('main-audio'); // 若有独立audio元素 if (video && video.readyState > 0) { video.playbackRate = rate; // 同步audio速率(若存在) if (audio) audio.playbackRate = rate; } // 对FLV播放器:调整render循环间隔 if (PlayerCore.flvPlayer) { PlayerCore.flvPlayer.setSpeed(rate); } // 对图片序列:调整setTimeout间隔 if (PlayerCore.imageLoader) { PlayerCore.imageLoader.fps = Math.round(24 * rate); // 基准24fps } };第三步:给FLV播放器加
setSpeed()
在FlvDemuxer类里加方法:setSpeed(rate) { this.speed = rate; // 修改render循环中的setTimeout间隔 if (this.renderTimer) { clearTimeout(this.renderTimer); this.renderTimer = setTimeout(() => this.render(), 1000 / (24 * this.speed)); } }
验证:播放任意MP4,点“2x”按钮,画面速度翻倍,音频音调升高——这是浏览器原生行为,无需额外处理。
6.2 Canvas截图:一行代码截当前帧,支持FLV/MP4/GIF
截图本质是<canvas>的toDataURL(),但难点在于:FLV和图片序列的帧数据不在<video>里,而在Canvas上。项目所有渲染最终都落到#main-canvas,所以只需抓它。
在
player-ui.js里加截图按钮:<button onclick="PlayerCore.takeScreenshot()">📷 截图</button>在
player-core.js里实现takeScreenshot():PlayerCore.takeScreenshot = function() { const canvas = document.getElementById('main-canvas'); if (!canvas) return; // 创建临时a标签触发下载 const link = document.createElement('a'); link.download = `screenshot-${Date.now()}.png`; link.href = canvas.toDataURL('image/png'); link.click(); // 清理内存 URL.revokeObjectURL(link.href); };关键适配:FLV和图片序列必须渲染到Canvas
检查Renderer模块,确保所有draw()方法都写入#main-canvas而非<video>。例如FLV解码后,Renderer.drawYUV()必须调用:// 正确:写入Canvas gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(0, 0, canvas.width, canvas.height); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); // 错误:写入video元素(不存在)
6.3 一个习惯:每次改完JS,强制走一遍“四环境验证”
从那以后我每次给播放器加功能,都强制走一遍这四个环境,少一个都算没测完:
file://双击:验证基础HTML/JS是否语法错误;- Python HTTP Server:验证M3U8/FLV的CORS和分片加载;
- Nginx生产配置:验证gzip压缩、缓存头、HTTPS重定向;
- iOS微信内嵌:验证
playsinline、触摸事件、静音autoplay。
这四个环境覆盖了99%的客户现场问题。比如上周加倍速功能,就在微信里发现playbackRate不生效——原来微信iOS版需要先video.muted = true才能设置速率。于是补了一行:
if (video && /MicroMessenger/i.test(navigator.userAgent)) { video.muted = true; }希望帮到你。
本文还有配套的精品资源,点击获取