- 音视频
- 前端
- UI组件
【免费下载链接】mediaelement
HTML5
MediaElement 除了播放器核心外,还内置了一套完整的工具函数(Utilities)与浏览器特性探测(Features),它们统一挂载在mejs.Utils和mejs.Features两个命名空间下,分别承担 DOM 操作、HTML 转义、URL/MIME 解析、时间码换算,以及浏览器识别与原生全屏能力检测等职责。本文基于官方文档 docs/utils.md 展开,并结合 src/js/utils/ 下的源码实现与 test/unit/utils.spec.js 的测试用例,逐个方法说明其签名、默认值、边界行为和源码级实现原理,帮助你既能直接调用这些工具,也能理解 MediaElement 内部是如何依赖它们来抹平各浏览器差异的。
一、命名空间与总体结构
所有工具函数都可以通过mejs.Utils.{name}访问,特性标志则通过mejs.Features.{name}访问。mejs命名空间定义在 src/js/core/mejs.js 中:该文件创建空对象mejs(当前版本号为7.0.7),挂到window.mejs上,并声明了播放器要代理的 HTML5 媒体属性(properties、readOnlyProperties)、方法(methods)、事件(events)和支持的媒体类型(mediaTypes,如audio/mp3、video/mp4、video/webm、video/ogg等)。
各工具模块在文件末尾统一把自己挂到mejs.Utils上,例如 src/js/utils/time.js 的最后几行:
mejs.Utils = mejs.Utils || {}; mejs.Utils.secondsToTimeCode = secondsToTimeCode; mejs.Utils.timeCodeToSeconds = timeCodeToSeconds; mejs.Utils.calculateTimeFormat = calculateTimeFormat; mejs.Utils.convertSMPTEtoSeconds = convertSMPTEtoSeconds;源码中工具分为四个模块:
| 模块 | 文件 | 职责 |
|---|---|---|
| DOM | src/js/utils/dom.js | 坐标、class 操作、淡入淡出、AJAX 等 DOM 工具 |
| General | src/js/utils/general.js | HTML 转义、防抖、事件拆分与创建等通用函数 |
| Media | src/js/utils/media.js | URL 绝对化、MIME 类型推断、扩展名处理 |
| Time | src/js/utils/time.js | 秒数与时间码互转、时间格式计算、SMPTE 解析 |
此外,src/js/utils/constants.js 负责浏览器与特性探测(下文 Features 部分);src/js/utils/polyfill.js 则在加载期为缺失的原生 API(CustomEvent、Object.assign、requestAnimationFrame、Element.closest、Node.remove等)做轻量补丁——很多 Utils 的实现正是依赖这些 polyfill,例如createEvent依赖CustomEvent,fadeIn/fadeOut依赖requestAnimationFrame。
二、DOM 工具:mejs.Utils 的 DOM 部分
官方文档指出,MediaElement.js已经内置了一些 polyfill 来替代 jQuery 的匹配/操作/AJAX 能力;但对于原生 API 无法直接对齐的部分,项目自行实现了等价方法。以下表格完整继承自 docs/utils.md 的 DOM 章节,并结合 src/js/utils/dom.js 的实现补充了源码细节:
| 方法 | 说明 |
|---|---|
offset(element) | 获取element的top和left坐标 |
hasClass(element, className) | 检查element是否带有className类 |
addClass(element, className) | 为element添加className类 |
removeClass(element, className) | 移除element上的className类 |
toggleClass(element, className) | 切换element上的className类(有则删、无则加) |
fadeIn(element, duration, callback) | 在duration毫秒内显示element(默认400),完成后执行callback(如有) |
fadeOut(element, duration, callback) | 在duration毫秒内隐藏element(默认400),完成后执行callback(如有) |
siblings(element, filter) | 基于filter条件(如有)获取element的所有兄弟节点 |
visible(element) | 检查element是否可见;不仅判断display: none,visibility: hidden也视为不可见 |
ajax(url, dataType, success, error) | 封装 AJAX 请求,dataType支持text、html、json、xml,成功后走success,失败走error |
坐标与 class 操作
offset基于getBoundingClientRect(),再加上pageXOffset/pageYOffset滚动量,返回文档坐标系下的{top, left}(src/js/utils/dom.js#L29-L34)。progress功能在计算拖拽滑块相对容器位置时就使用了它,见 src/js/features/progress.js#L270。
class 操作在初始化时做了双路径选择:若浏览器支持classList,直接走classList.contains/add/remove;否则回退到基于\b词边界正则的className字符串拼接与替换(src/js/utils/dom.js#L36-L52)。toggleClass只是hasClass+addClass/removeClass的组合。
fadeIn / fadeOut:requestAnimationFrame 驱动的透明度动画
两者签名一致,duration默认400毫秒。实现上用requestAnimationFrame逐帧推进:fadeOut从当前 opacity(若未设置先置为1)按1 - progress/duration递减到0;fadeIn则从0按progress/duration递增到1,动画帧数结束后若传入的是函数形式的callback就会执行一次(src/js/utils/dom.js#L62-L105)。源码注释说明这段实现参考了 vanilla-helpers 项目。
siblings 与 visible
siblings(el, filter)从父节点firstChild开始,沿着nextSibling链收集所有兄弟节点;注意源码中filter是一个函数而非 CSS 选择器,传入时以filter(el)的返回值决定是否保留该节点(src/js/utils/dom.js#L107-L116)。
visible通过offsetWidth/offsetHeight判断元素是否实际占据布局空间,visibility: hidden的元素由于尺寸为0会被判为不可见,这正是文档中“超越display: none检查”说法的来源(src/js/utils/dom.js#L118-L123)。
ajax:回调式的 XHR 封装
ajax内部创建XMLHttpRequest(旧 IE 回退到ActiveXObject),按dataType设置Accept头:text对应text/plain、json对应application/json, text/javascript、html对应text/html、xml对应application/xml, text/xml。请求为GET,readyState === 4且status === 200时,按类型把响应解析为对象(JSON.parse/responseXML/ 原始文本)后回调success(data);非200则回调error(xhr.status),并用completed标志防止重复触发(src/js/utils/dom.js#L125-L187)。
此外源码中还有一个未在文档表格中列出的loadScript(url):创建异步<script>注入head,成功/失败后自动移除节点,并以 Promise 形式返回(src/js/utils/dom.js#L12-L27),可用于动态加载 DASH、HLS 等外部播放引擎脚本。
三、General 工具:常用小函数集合
文档描述:“有时我们需要完成 HTML 转义、判断值类型等常见任务,MediaElement.js也实现了一些方法来完成它们。”完整方法表如下(继承自 docs/utils.md),实现位于 src/js/utils/general.js:
| 方法 | 说明 |
|---|---|
escapeHTML(input) | 转义&、<、>、"四个字符,防止 XSS 攻击 |
debounce(callback, wait, immediate) | 在wait时间窗口内执行callback;immediate为true时绕过等待立即执行(默认false) |
isObjectEmpty(object) | 检查object是否为空 |
splitEvents(events, id) | 把空格分隔的events字符串拆分为document(d)与window(w)两类事件;可传id追加命名空间 |
createEvent(eventName, target) | CustomEvent的封装,传入事件名与可选target创建事件 |
isNodeAfter(sourceNode, targetNode) | 判断targetNode是否出现在 DOM 中sourceNode之后 |
isString(input) | 判断input是否为字符串 |
escapeHTML 与参数校验风格
escapeHTML用一个映射表把& < > "替换为&、<、>、",正则[&<>"]一次遍历完成(src/js/utils/general.js#L10-L26)。单元测试覆盖了典型场景:'<p>Hello, "world" & welcome!</p>'被转义为'<p>Hello, "world" & welcome!</p>';传入非字符串则抛出Error(test/unit/utils.spec.js#L165-L182)。
值得注意的是这些函数统一的参数校验风格:debounce要求第一参是函数、第二参是数字,否则直接抛错;createEvent要求事件名是字符串;escapeHTML要求入参是字符串。测试文件对每个“只接受某类型参数”的场景都有对应断言(如 test/unit/utils.spec.js#L44-L62),这是调用时的实际契约。
debounce:来自 underscore 的防抖
debounce(func, wait, immediate = false)是典型的防抖实现:每次调用先clearTimeout再重新计时;immediate为true时首次调用立即执行、后续wait窗口内的调用被吞掉(src/js/utils/general.js#L28-L56)。isObjectEmpty用Object.getOwnPropertyNames判断自有属性数量是否为零;isString就是typeof value === 'string';isNodeAfter基于compareDocumentPosition(target) & 2(Node.DOCUMENT_POSITION_PRECEDING)判断节点先后顺序(src/js/utils/general.js#L133-L150)。
splitEvents:给事件挂上播放器命名空间
splitEvents(events, id)把形如'beforeunload hashchange resize .mouseup .volumechange.test'的空格分隔字符串,按内置正则rwindow(匹配beforeunload、hashchange、resize、storage、pagehide等 window 级事件)分成d(document 监听)和w(window 监听)两组字符串;若传入了id(播放器 ID,如mep_0),会给每个事件追加.id命名空间,方便之后统一解绑。以点开头的事件(如.mouseup)会同时出现在两组中。单元测试给出的真实结果是(test/unit/utils.spec.js#L80-L101):
const events = 'beforeunload hashchange message resize storage .mouseup .volumechange.test'; general.splitEvents(events, 'mep_0'); // result.d: '.mouseup.mep_0 .volumechange.test.mep_0' // result.w: 'beforeunload.mep_0 hashchange.mep_0 message.mep_0 resize.mep_0 storage.mep_0 .mouseup.mep_0 .volumechange.test.mep_0'createEvent:第三方渲染器统一事件模型的关键
createEvent(eventName, target, isIframe)返回一个CustomEvent,其detail中携带{target, isIframe};如果事件名带命名空间(如'customevent.namespace'),正则会把名字拆成事件本体与namespace写入 detail(src/js/utils/general.js#L104-L126)。
它最典型的用途在第三方视频渲染器中:src/js/renderers/dailymotion.js、src/js/renderers/soundcloud.js、src/js/renderers/twitch.js、src/js/renderers/facebook.js 都把各平台 SDK 的回调包装成标准媒体事件再dispatch,例如 Dailymotion 渲染器中:
const event = mejs.Utils.createEvent('timeupdate', dm);这样无论底层是 HTML5<video>还是第三方服务,上层播放器(time、progress、tracks等功能)都能用同一套 MediaElement API 事件模型工作——这正是项目“common HTML5 MediaElement API”设计目标在工具层的落点。
四、Media 工具:URL、MIME 与扩展名
这一组函数解决“给一个 URL/类型,知道它到底是什么媒体”的问题,实现位于 src/js/utils/media.js:
| 方法 | 说明 |
|---|---|
absolutizeUrl(path) | 把相对path补全为完整 URL |
formatType(url, type) | 基于url与 MIMEtype推断具体媒体的格式 |
getMimeFromType(type) | 在type含编解码器声明时取出 MIME 部分(video/mp4; codecs="avc1.42E01E, mp4a.40.2"变为video/mp4) |
getTypeFromFile(url) | 基于url结构推断媒体类型 |
getExtension(url) | 从url中取出媒体文件扩展名 |
normalizeExtension(extension) | 把媒体扩展名归一化为标准形式 |
absolutizeUrl:用一个隐藏的<a>让浏览器解析 URL
实现非常巧妙:新建一个<div>,写入<a href="...">(注意 href 先经过escapeHTML处理),再读取firstChild.href——浏览器会自动把相对路径解析为绝对 URL(src/js/utils/media.js#L13-L22)。测试中以http://localhost为页面基址验证:absolutizeUrl('/media/demo.html')得到'http://localhost/media/demo.html',非字符串入参会抛Error。
getTypeFromFile 与可插拔的 typeChecks 注册表
getTypeFromFile的逻辑是两级推断(src/js/utils/media.js#L58-L91):
- 先遍历
mejs.Utils.typeChecks注册表:这是一个函数数组,每个函数接收 URL 并返回 MIME 类型或 falsy;第一个命中的结果直接返回。第三方代码可以往mejs.Utils.typeChecks里 push 自己的识别规则(比如识别m3u8/mpd地址),测试用例正是这样注入.mp4、.mp3规则的; - 未命中则走扩展名兜底:取
getExtension(url)的结果,经normalizeExtension归一化后映射——mp4/m4v/ogg/ogv/webm/mpeg映射为video/{ext}、mov映射为video/quicktime、mp3/oga/wav/mid/midi映射为audio/{ext};都不匹配时默认返回video/mp4。
单元测试验证了含查询串的 URL 也能正确取扩展名:http://example.com/media2.mp4?x=1&y=2→video/mp4,media.midi→audio/midi。
getExtension 与 normalizeExtension
getExtension先截断?之后的查询串,再取路径最后一段,返回最后一个.之后的部分;没有.时返回空串(src/js/utils/media.js#L99-L107)。测试用例包括m3u8(带查询串)、html以及无扩展名的lorem ipsum。
normalizeExtension的归一化规则是:mp4/m4v → mp4,webm/webma/webmv → webm,ogg/oga/ogv → ogg,其余原样返回(src/js/utils/media.js#L115-L136),测试验证了m4v→mp4、webma→webm、oga→ogg、m3u8→m3u8等映射。
formatType(url, type)的语义是:只有url而无type时才用getTypeFromFile从 URL 推断;只要传了type就直接返回type本身(src/js/utils/media.js#L31-L33),测试用例也印证了带 codecs 的audio/mp3; codecs=...会原样返回。getMimeFromType则只截掉第一个;之后的部分,用于在<source type="...">携带编解码器声明的场景中取出纯净 MIME。
五、Time 工具:时间码与格式计算
时间工具位于 src/js/utils/time.js,服务于进度条时间显示、章节标记等场景:
| 方法 | 说明 |
|---|---|
secondsToTimeCode(time, forceHours, showFrameCount, fps, secondsDecimalLength) | 把数字time格式化为'00:00:00'形式;forceHours为true时强制显示小时位,showFrameCount为true时追加帧数;fps默认25,secondsDecimalLength控制小数位数 |
timeCodeToSeconds(time, fps) | 把'00:00:00'时间码字符串转为秒数;fps默认25 |
calculateTimeFormat(time, options, fps) | 根据播放器options(其中的timeFormat)计算应使用的时间格式;time理想为数字,否则按0处理;fps默认25 |
convertSMPTEtoSeconds(SMPTE) | 把 SMPTE(电影电视工程师协会)时间码转为秒数 |
secondsToTimeCode:支持丢帧(drop-frame)时间码
该函数签名比文档多了第 6 个参数timeFormat(默认'hh:mm:ss'),内部先判断isDropFrame(fps):当 fps 不是整数(如 29.97、29.976 这类 NTSC 丢帧帧率)时,会按 6% 规则(dropFrames = Math.round(fps * 0.066666))在每个十分钟边界补偿丢帧,且帧分隔符使用;而非:(src/js/utils/time.js#L11-L109)。
单元测试覆盖了完整行为谱(test/unit/utils.spec.js#L192-L227):
time.secondsToTimeCode(36) // '00:36' time.secondsToTimeCode(70) // '01:10' time.secondsToTimeCode(3600) // '01:00:00' time.secondsToTimeCode(36, true) // '00:00:36'(forceHours) time.secondsToTimeCode(36.45, false, true, 32) // '00:36:14'(32fps 下的帧数) time.secondsToTimeCode(70.89, true, true, 40) // '00:01:10:36' time.secondsToTimeCode(3600.234, true, true, 25, 0, 'hh:mm:ss:ff') // '01:00:00:06' time.secondsToTimeCode(36.45, false, true, 32.46) // '00:36;31'(丢帧,分号分隔) time.secondsToTimeCode({}) // '00:00'(非数字入参归零,不抛错)非数字、负数入参会被静默归零而不是抛异常,这与escapeHTML等严格校验的风格形成对比,调用时注意区分。
timeCodeToSeconds:反向转换
要求入参必须是符合\d{2}(\:\d{2}){0,3}的字符串,支持 1 到 4 段(纯秒 / 分:秒 / 时:分:秒 / 时:分:秒:帧),遇到丢帧分隔符;会先替换成:;丢帧帧率下同样做 6% 补偿计算,结果保留 3 位小数。测试示例(test/unit/utils.spec.js#L229-L268):
time.timeCodeToSeconds('00:36') // 36 time.timeCodeToSeconds('01:00:00') // 3600 time.timeCodeToSeconds('00:00:36:14', 32) // 36.438 time.timeCodeToSeconds('01:00:00;05') // 3600.2非字符串或格式不符会抛TypeError。
calculateTimeFormat:自动补齐 timeFormat
播放器默认的时间格式(如mm:ss)对时长超过一小时的媒体是不够的,calculateTimeFormat(time, options, fps)会根据实际时长把options.timeFormat自动前置补齐:它从帧→秒→分→时的顺序检查,只要低一位已在格式中、而当前位数值大于 0,就在前面插入该位(src/js/utils/time.js#L188-L243)。测试验证:mm:ss格式遇到 36 秒保持mm:ss;遇到 3600 秒则变为hh:mm:ss;结果会写回options.timeFormat(test/unit/utils.spec.js#L270-L301)。
convertSMPTEtoSeconds
处理形如hh:mm:ss.fff的 SMPTE 时间码:先把逗号归一为小数点,再从右向左按 60 的幂累加,最后按原始小数位数四舍五入返回数字。测试示例:convertSMPTEtoSeconds('00:12.34')返回12.34;非字符串抛错(test/unit/utils.spec.js#L303-L319)。
六、Features:浏览器识别与全屏能力探测
文档描述:MediaElement.js提供了一些标志/方法,用于判断用户处于哪种浏览器、支持哪种类型的全屏等,全部通过mejs.Features.{name}访问。完整清单如下(继承自 docs/utils.md):
mejs.Features.isiPad/isiPhone/isiOS/isAndroidmejs.Features.isIE/isEdge/isChrome/isFirefox/isSafarimejs.Features.isStockAndroid(原生 Android 浏览器)mejs.Features.hasMSE(MediaSource Extensions)mejs.Features.supportsNativeHLSmejs.Features.supportsPointerEventsmejs.Features.hasiOSFullScreen/hasNativeFullscreenmejs.Features.hasWebkitNativeFullScreen/hasMozNativeFullScreen/hasMsNativeFullScreen/hasTrueNativeFullScreenmejs.Features.nativeFullScreenEnabledmejs.Features.fullScreenEventNamemejs.Features.isFullScreen()mejs.Features.requestFullScreen()mejs.Features.cancelFullScreen()
这些标志的探测逻辑集中在 src/js/utils/constants.js:
- UA 检测:
UA = navigator.userAgent.toLowerCase(),用正则匹配ipad/iphone/ipod/android/chrome/firefox/safari等;IS_SAFARI会排除 Chrome(因为 Chrome 的 UA 也含safari);IS_EDGE通过'msLaunchUri' in navigator && !('documentMode' in document)判断,IS_STOCK_ANDROID匹配^mozilla/\d+\.\d+\s\(linux;\su;前缀。 - 能力检测:
hasMSE检查'MediaSource' in window(DASH 渲染器依赖它);supportsNativeHLS目前仅 Safari 与 IE Edge 被判为原生支持 HLS(即决定是否加载 hls.js 的关键依据);supportsPointerEvents固定为true(注释说明主流浏览器包括 IE11 均已支持,不再需要检测)。 - passive 事件支持:
supportsPassiveEvent用“给addEventListener传一个带 getter 的 options 对象,看 getter 是否被触发”的技巧来探测passive选项支持,用于进度条拖拽时避免浏览器把touchmove当可滚动手势而延迟响应(src/js/utils/constants.js#L23-L36)。 - 全屏 API 矩阵:通过创建一个
video元素探测四个前缀 API——webkitEnterFullscreen(iOS 全屏)、requestFullscreen(W3C 标准)、webkitRequestFullScreen、mozRequestFullScreen、msRequestFullscreen,并据此决定fullScreenEventName(webkitfullscreenchange/fullscreenchange/MSFullscreenChange),同时定义isFullScreen()、requestFullScreen(el)、cancelFullScreen()三个跨浏览器统一函数;对 mac os x 10_5 这类“自称支持实则不可用”的环境做了降级处理(src/js/utils/constants.js#L49-L126)。
从源码结构看,mejs.Features实际比文档清单多出两个标志:isiPod和supportsPassiveEvent(见 src/js/utils/constants.js#L138-L164 的挂载段),调用时可直接使用。Features 在渲染器中也有直接消费,例如 Facebook 渲染器用mejs.Features.isiPhone判断 iOS 上是否需要强制显示 poster(src/js/renderers/facebook.js#L92),全屏功能 src/js/features/fullscreen.js 则完全依赖这组标志决定走原生全屏还是浏览器内模拟全屏。
七、Polyfill 依赖与使用建议
Utils 并非完全独立,它们运行在 src/js/utils/polyfill.js 建立的基线之上:CustomEvent(createEvent依赖)、Object.assign、String.prototype.startsWith(splitEvents依赖)、Element.matches、Element.closest、requestAnimationFrame(fadeIn/fadeOut依赖)、Node.prototype.remove、children属性(IE9/Safari)以及 Firefox iframe 下getComputedStyle返回 null 的补丁。理解这些 polyfill 有助于解释为何工具函数可以在较老浏览器中工作。
几点实战建议:
- 第三方播放器集成:如果你要写一个新的渲染器(对接某个视频平台),参照 src/js/renderers/soundcloud.js 等现有实现,把平台事件桥接为
mejs.Utils.createEvent(...)再 dispatch,即可无缝接入 MediaElement 的统一事件模型; - 媒体类型识别:HLS(
m3u8)、DASH(mpd)等特殊格式可以往mejs.Utils.typeChecks注册识别函数,而不是改动核心代码; - 时间显示:自定义播放器 UI 的时间标签时,直接复用
secondsToTimeCode+calculateTimeFormat的组合,可自动获得小时位补齐与丢帧帧率支持; - 安全:任何把用户/远端内容写入 DOM 的地方(标题、描述、字幕文本)都应先过
escapeHTML,这是项目内部生成 HTML 字符串时防 XSS 的标准做法。
以上所有函数的行为边界(默认值、抛错条件、归零处理)均可在 test/unit/utils.spec.js 中找到对应断言,配合 src/js/utils/ 源码可直接作为二次开发的参考契约。
- 音视频
- 前端
- UI组件
【免费下载链接】mediaelement
HTML5
相关推荐
Cargo 特性(Features)选择指南:深入理解 `--features`、`--all-features` 与 `--no-default-features`
Cargo 特性(Features)选择指南:深入理解 features 、 all features 与 no default features 导读 本文聚
开发工具包管理器CLI构建工具plate 第二阶段 Utility Ring 执行全解:@platejs/utils 与 @udecode/react-utils、@udecode/utils 的 TDD 覆盖与运行时缺陷修复
plate 第二阶段 Utility Ring 执行全解:@platejs/utils 与 @udecode/react utils、@udecode/util
前端富文本UI组件features/production.toml
features/production.toml core_expressions = true datetime_expressions = true enc
大数据数据分析后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考