feat: 集成edu-core模块并重构课程详情页视频播放

- 添加edu-core本地依赖,用于视频播放组件
- 重构课程详情页,使用CourseVideo组件替换原有视频播放逻辑
- 优化课程列表页布局和样式,修复边框单位问题
- 更新manifest.json支持多方向屏幕旋转
This commit is contained in:
2026-02-09 10:29:59 +08:00
parent 671b7eb63d
commit 0e900d19be
33 changed files with 11533 additions and 98 deletions

View File

@@ -0,0 +1,262 @@
## 1.8.22025-12-17
* 1、修复设置initialTime在播放hls流无效的问题
* 2、修复插件在pc端点击无反应的问题
* 3、修复插件发布h5到线上后使用renderjs渲染类型报错getCurrentPage is not defined的问题
* 4、修复ios部分版本的系统向app通信无反应的问题
* 5、插件内js文件ES6+语法转换为ES5语法兼容低版本webview但js文件可读性变差了如果需要修改源码的开发者请下载示例项目源代码放在libs中
## 1.8.12025-11-11
* 修复切换src可能会报错的问题
* 优化自动判断解码器类型函数
* capture方法增加是否展示弹窗配置
* 优化视频内点击事件,取消可能会有的点击延迟
* 优化css样式
## 1.8.02025-10-29
* 1、增加jsmpeg.js解码器
* 2、renderjs渲染模式下增加组件插槽
* 3、height属性增加inherit配置
## 1.7.82025-10-27
* 修复调用unload后不触发destroyed的问题
* 增加hlsConfig配置
*
## 1.7.72025-10-22
* 修复H5在history路由模式下无法加载组件的问题
* 优化头部控制栏左边插槽初始位置
## 1.7.62025-10-10
* 增加弹幕过滤规则功能
* 优化弹幕时间轴,减少弹幕丢失
* 增加电脑端手势事件模拟
* 弹幕配置放在custom统一属性具体看文档
* 优化手势控制音量和亮度的卡顿问题
* 增加顶部控制栏返回按钮自定义和标题自定义控制,增加左右插槽功能
## 1.7.52025-09-28
* renderjs渲染模式现在已经支持在vue3下开启360全景视频
* 增加全屏顶部和底部安全区域间距
* 增加全屏左右安全区域对称
## 1.7.42025-09-26
* 增加renderType渲染模式属性
* 增加renderjs渲染模式ios默认使用renderjs渲染模式
* renderjs渲染模式渲染模式有限制详情请看文档
## 1.7.32025-09-25
* 修复更改custom配置后全屏和非全屏元素标记失效的问题
* 增加发送弹幕是否需要边框配置
* 修复duration设置不生效的问题
* 修复跟随控制栏插槽元素全屏和非全屏标记无效的问题
* 修复IOS UIWbview内核无法操作iframe导致视频不加载的问题
* 增加IOS WKWbview内核无法使用该插件的解决方案
## 1.7.22025-09-22
* 修复seek方法报错的问题
* 隐藏非全屏时的分P按钮
* 增加插槽非全屏和全屏元素标记
## 1.7.12025-09-19
* 这是一次大更新,组件已完全重构,不想折腾的同学就不要更新下载了
* 控制栏样式更改
* 弹幕增加顶端弹幕和底端弹幕,优化弹幕绘制
* 内置弹幕配置修改功能
* 内置弹幕发送功能
* 增加360全景视频播放功能
* 增加字幕功能
* 增加画质切换功能
* 增加分P选择功能
* 增加截图显示和保存功能
* 增加手势控制功能
* 增加消息提示功能
* 增加自定义工具栏功能
* 增加屏幕锁定功能
* 其它功能请自行体验或者看文档
## 1.7.02025-07-18
* 优化IOS环境下可能会出现的flv.js静态资源加载失败的问题现在IOS端不再加载flv.js因为不支持加载了也没用
## 1.6.92024-10-10
* 修复动态设置属性无效的问题
* 增加双击屏幕播放/暂停和控制开关属性
* 增加长按屏幕倍速播放和控制开关属性
## 1.6.82024-09-19
* 修复设置部分属性无效的问题
## 1.6.72024-08-26
* 修复设置高度不生效的问题
* 修复主动调用全屏方法不生效的问题
## 1.6.62024-06-24
* 修复初始化传入视频链接无反应的问题
## 1.6.52024-06-21
* 修复vue3app端全屏时间显示格式异常的问题
* 修复部分视频切换视频加载错误的问题
## 1.6.42024-05-11
* 修复timeShow设为fasle后进度条不走的问题
* 修复barrages改变后弹幕不刷新的问题
* 引入hls.mjs文件
## 1.6.32024-04-22
* 修复vue3和vue2的renderJS环境watcher触发时间差异引起的问题
## 1.6.22024-04-22
* 修复app报错的问题
## 1.6.12024-04-18
* 修复vue3切换页面会显示多个控制栏的问题
## 1.6.02024-04-10
* drawBarrage增加多条弹幕绘制
* ybplayer.js和ybbarrage.js文件使用import引入
## 1.5.92024-04-01
* 升级flv.min.js到1.6.2
* 升级hls.min.js 到1.5.7
* 添加flv.js的监听事件若干
## 1.5.82024-03-22
* 因为许多人反映flv会有闪烁的问题所以将flv的重连代码删除改为抛出statisticsinfo事件
* 增加durationChange事件
* 修复播放直播时,能获取到播放时长的问题
## 1.5.72024-03-12
* 修复app端error事件无法抛出的问题
* 添加barrageChange事件
## 1.5.62024-03-08
* 修复倍速设置初始化时无效的问题
* 修复视频(直播流卡住事件)不起作用的问题
## 1.5.52024-03-02
* 添加vue3兼容因为vue3不能引入非标准模块的js文件插件引入js的方法改为script引入导致h5不能使用history路由模式
* 修复h5画中画设置按钮不能自动改变状态的问题
## 1.5.42024-03-01
* 增加reload重加载方法
* 增加seizing直播流播放卡住事件
## 1.5.32023-12-27
* 修复动态设置controls无效的问题
## 1.5.22023-11-30
* 修复controls和timeShow同时设为false时会报错的问题
* 修改css文件引入方式
## 1.5.12023-11-24
* 修改flvConfig中部分配置初始值为空解决无法自动判断的问题
## 1.5.02023-11-07
* 修复动态变更控制栏静音按钮、设置按钮、全屏按钮后,点击事件无效的问题
## 1.4.92023-09-15
* 解决3.8.12版本或相近版本的编辑器会出现_typeof is not defined的报错的问题
## 1.4.82023-09-11
* 不再通过script引入文件
## 1.4.72023-08-18
* 解决mp4格式视频preload设置无效的问题
## 1.4.62023-08-16
* 解决APP初始化时显示播放图标的问题
## 1.4.52023-08-15
* 解决H5端播放m3u8视频页面销毁时还继续请求的问题
## 1.4.42023-08-15
* 隐藏APP端视频初始化时的播放图标
* 解决视频初始化时进度条在中间的问题
* 优化进度条样式,使其与上方控制按钮对齐
## 1.4.32023-08-10
* 修复rageChange和volumeChange事件监听无效的问题
## 1.4.22023-08-03
* 解决H5端退出页面再进入会报错的问题
* 增加页面销毁时自动销毁视频
## 1.4.12023-07-17
* 取消resize事件
* 优化窗口大小改变的监听逻辑
* 优化初始化方法,避免报错
* isLive为true关闭进度条
## 1.4.02023-07-08
* 解决切换src后插槽点击事件失效的问题
## 1.3.92023-07-06
* 修复设置duration时loop属性无效的问题
* 优化初始化代码,解决视频组件无法循环遍历的问题
## 1.3.82023-07-05
* 优化组件结构,解决层级问题
## 1.3.72023-07-04
* 修复设置项无法点击的问题
* 优化封面
## 1.3.62023-07-04
* 增加enableBlob属性
* 修复插槽内容会被遮挡的问题
## 1.3.52023-06-30
* 增加播放按钮、播放时间、静音按钮、设置按钮、全屏按钮的显示控制
* 新增duration属性设置播放时长
* 将flvConfig属性hasVideo默认值设为true
* 修复封面闪烁消失的问题
* 不设置高度时,组件自适应高度
## 1.3.42023-06-08
* 修复直接给组件赋值src会报跨域的问题
## 1.3.32023-04-26
* flvConfig 新增几种属性配置
## 1.3.22023-04-24
* 修复设置progressShow导致控制栏重叠的问题
* 修复initialTime设置不生效的问题
* 新增canplaythrough、loadeddata、loadstart事件
## 1.3.12023-04-22
* 新增crossOrigin属性
* 修复h5可直接播放的视频还会跨域的问题
## 1.3.02023-04-20
* 修复配置progressShow属性不生效的问题
## 1.2.92023-04-18
* 修复objectFit不生效得问题
## 1.2.82023-04-08
* 修复全屏时切换视频顶部title不显示的问题
## 1.2.72023-04-08
* 优化视频切换
## 1.2.62023-04-06
* 新增title属性用于全屏时顶部标题展示
* 新增prevBtnShow、nextBtnShow属性用于显示切换上一个或下一个视频按钮
* 新增prevBtnClick、nextBtnClick事件用于切换上一个或下一个视频
## 1.2.52023-04-01
* 修复YBPlayer可能会报错的问题
* 修复FLV无法播放的问题
## 1.2.42023-03-31
* 添加controlsChange事件
* 修复拖动进度条抖动的问题
## 1.2.32023-03-27
* 新增一种控制栏插槽,该插槽内容会随控制栏一起消失/显示
## 1.2.22023-03-26
* 修复动态添加视频组件无法播放的问题
## 1.2.12023-03-25
* 修复创建多个视频组件会冲突的问题
## 1.2.02023-03-24
* 优化设置菜单点击反馈异常的问题
## 1.1.92023-03-24
* 将组件js化方便用于普通html
* 新增截图功能
* 新增设置菜单控制属性
* 取消原本的npm安装模块的方式现在所需要的库已全部集成
## 1.1.82023-03-16
* 新增stop方法用于注销视频
## 1.1.72023-03-16
* fullscreenChange事件新增type属性根据此属性可判断全屏是否使用css模拟
## 1.1.62023-03-16
* 优化全屏事件
## 1.1.52023-03-15
* 优化模块加载,可以根据需求自己下载相应模块
* canplay事件和loadmetadata事件新增视频宽高信息
* 修复方法名错误的问题
## 1.1.42023-03-11
* 更改switchFullscreen方法名称
## 1.1.32023-03-11
* 重构视频播放器,减少不必要的功能
* 短视频功能取消
* 简化控制栏
* 播放列表功能取消现在更接近原生VIDEO
* 新增播放flv功能
* 更改部分属性、事件、方法的名称
## 1.1.22022-12-17
* 对于不支持全屏api的系统 使用css来模拟全屏该全屏方法限制较多效果不如全屏api好如果有更好办法的小伙伴希望不吝赐教
* 注意使用css模拟全屏需要关闭原生导航栏且在tabbar页面全屏时需要手动关闭原生tabbar
## 1.1.12022-10-12
* 优化app端的全屏模式
* 短视频模式添加全屏按钮
* 注意新的全屏模式需要高版本编辑器和设备支持我用的是3.4.7的编辑器和安卓7.1.2的模拟器,大家可以以此为参考
## 1.1.02022-09-24
* 修复上次更新的bug
* 去掉高版本编辑器报错
## 1.0.92022-09-23
* 优化判断浏览器是否能播放m3u8格式视频
## 1.0.82022-09-23
* 解决高版本hbuilderX会报错的问题
* 优化不同浏览器的播放功能,实现同步播放
* 解决ios端无法播放的问题
* 修复暂停播放等操作会触发弹幕初始化的问题
## 1.0.72022-08-17
* 修复播放倍率设置不生效的问题
## 1.0.62022-08-17
* 更改内部使用组件的名称(用在自己的项目上才发现组件名冲突了,没有冲突的小伙伴可以不下载)
## 1.0.52022-08-17
* 修复播放m3u8格式视频后继续播放其它格式视频异常的bug
## 1.0.42022-07-22
* 新增控制弹幕显示上下间距属性
## 1.0.32022-07-21
* 短视频模式新增关闭自定义内容按钮
* 优化一些内容
## 1.0.22022-07-21
* 修复一些bug
## 1.0.12022-07-21
* 短视频模式新增模糊背景显示
* 删除短视频模式下的全屏按钮
## 1.0.02022-07-20
* 发布第一版,使用前请看使用须知

View File

@@ -0,0 +1,498 @@
<template>
<view
:class="'iframe' + dataId"
class="find_iframe"
:data-sandbox="sandbox"
:data-allow="allow"
:data-allowfullscreen="allowfullscreen"
:data-frameborder="frameborder"
:data-loadingShow="loadingShow"
:data-console="console"
:data-iframeStyle="iframeStyleString"
:data-iframeClass="iframeClassString"
:data-crossOrigin="crossOrigin"
:data-setDataName="setDataName"
:data-overrideUrlLoadingOptions="overrideUrlLoadingOptionsString"
:ready="ready" :change:ready="ComIframe.readyWatcher"
:isDestroy="isDestroy" :change:isDestroy="ComIframe.destroyWatcher"
:src="src" :change:src="ComIframe.srcWatcher"
:sandbox="sandbox" :change:sandbox="ComIframe.sandboxWatcher"
:allow="allow" :change:allow="ComIframe.allowWatcher"
:allowfullscreen="allowfullscreen" :change:allowfullscreen="ComIframe.allowfullscreenWatcher"
:frameborder="frameborder" :change:frameborder="ComIframe.frameborderWatcher"
:iframeStyle="iframeStyle" :change:iframeStyle="ComIframe.iframeStyleWatcher"
:iframeClass="iframeClass" :change:iframeClass="ComIframe.iframeClassWatcher"
:jump="jump" :change:jump="ComIframe.jumpWatcher"
:assignUrl="assignUrl" :change:assignUrl="ComIframe.assignUrlWatcher"
:loadingStatus="loadingStatus" :change:loadingStatus="ComIframe.loadingWatcher"
:evalJS="evalJSString" :change:evalJS="ComIframe.evalJSWatcher"
:evalCSS="evalCSSString" :change:evalCSS="ComIframe.evalCSSWatcher"
:setData="setDataValue" :change:setData="ComIframe.setDataWatcher"
:overrideUrlLoading="overrideUrlLoadingOptionsString" :change:overrideUrlLoading="ComIframe.overrideUrlLoadingWatcher">
</view>
</template>
<script>
export default {
props: {
dataId: {
type: String,
default () {
return new Date().getTime().toString() + Math.round(Math.random() * 10000)
}
},
src: {//链接
type: String,
default: ''
},
sandbox: {//沙盒模式 原生属性
type: String,
default: 'allow-same-origin allow-scripts allow-forms allow-top-navigation-by-user-activation allow-popups allow-modals'
},
allow: {//允许一些操作 原生属性
type: String,
// default: 'autoplay; fullscreen; encrypted-media; picture-in-picture'
default: ''
},
allowfullscreen: {//是否允许全屏 原生属性
type: Boolean,
default: true
},
//跨域属性 anonymous-它有一个默认值。它定义了将在不传递凭据信息的情况下发送CORS请求 use-credentials-将发送带有凭据、cookie 和证书的 cross-origin 请求
crossOrigin: {
type: String,
default: ''
},
frameborder: {//iframe边框 原生属性
type: String,
default: '0'
},
iframeClass: {//iframe样式
type: [String, Object],
default: ''
},
iframeStyle: {//iframe样式
type: [String, Object],
default: ''
},
loadingShow: {//展示loading
type: Boolean,
default: true
},
console: {//控制console
type: String,
default: 'log warn error'
},
overrideUrlLoadingOptions: {//仅支持app
type: Object,
default: () => {
return {
effect: '',
mode: '',
match: '',
exclude: ''
}
}
}
},
computed: {
overrideUrlLoadingOptionsString () {
return JSON.stringify(this.overrideUrlLoadingOptions)
},
iframeStyleString () {
return JSON.stringify(this.iframeStyle)
},
iframeClassString () {
return JSON.stringify(this.iframeClass)
}
},
data () {
return {
ready: '',
jump: 0,
isDestroy: false,
evalJSString: '',//js字符串
evalCSSString: '',//注入css字符串
assignUrl: '',//跳转链接
loadingStatus: -1,//加载进度提控制
evalJSTask: new Map(),//注入js临时队列表
setDataName: '',
setDataValue: ''
}
},
mounted () {
this.$nextTick(function () {
setTimeout(() => {
this.ready = this.dataId
}, 100)
})
},
methods: {
//返回上一页
back () {
this.jump = 0
this.$nextTick(function () {
this.jump = -1
})
},
//调用内部iframe跳转链接
assign (url) {
this.assignUrl = url
},
//显示进度条加载
showLoading () {
this.loadingStatus = -1
this.$nextTick(function () {
this.loadingStatus = 1
})
},
//关闭进度条加载
hideLoading () {
this.loadingStatus = -1
this.$nextTick(function () {
this.loadingStatus = 0
})
},
//销毁
destroy () {
this.isDestroy = false
this.$nextTick(() => {
this.isDestroy = true
})
},
//注入js
evalJS (str) {
if ( this.evalJSString ) {//如果又正在进行注入的js任务
const id = new Date().getTime().toString() + Math.round(Math.random() * 10000)
this.evalJSTask.set(id, str)//加入等待队列
} else {
this.evalJSString = str
}
},
//注入css
evalCSS (str) {
this.evalCSSString = ''
this.$nextTick(() => {
this.evalCSSString = str
})
},
setData (key, value) {
this.setDataName = key
this.setDataValue = ''
this.$nextTick(() => {
this.setDataValue = value
})
},
loadstart (e) {
this.$emit('loadstart', e)
},
loaded (e) {
this.$emit('loaded', e)
},
callError (e) {
this.$emit('error', e)
},
backError () {
this.$emit('backerror')
},
callDestroy () {
this.$emit('destroyed')
},
overrideUrlLoading (e) {
this.$emit('overrideurlloading', e)
},
message (e) {
this.$emit('message', e)
},
callEvalJS (e) {//注入js回调
this.evalJSString = ''
if ( this.evalJSTask.size ) {//如果等待队列有值并且当前没有正在执行的注入js任务
const firstEntrie = this.evalJSTask.entries().next().value//获取等待队列中的第一个
this.evalJS(firstEntrie[1])//执行任务
this.evalJSTask.delete(firstEntrie[0])//删除等待队列
}
if ( e.status == 'error' ) this.callError(e.message)
}
}
}
</script>
<!-- #ifdef APP-VUE || H5 -->
<script lang="renderjs" type="module" module="ComIframe">
export default {
data () {
return {
dom: null,
iframe: null,
loading: null,
iframeSrc: '',
backTimer: null
}
},
mounted () {
window.addEventListener('message', this.messageListener);
},
beforeDestroy () {
this.destoryIframe()
},
methods: {
messageListener (e) {
this.callMethod('message', {origin: e.origin, data: e.data})
},
destoryIframe () {
window.removeEventListener('message', this.messageListener)
if ( this.loading ) {
this.loading.remove()
this.loading = null
}
if ( this.iframe ) {
this.iframe.remove()
this.iframe = null
}
this.callMethod('callDestroy')
},
showLoadingRender () {
if ( this.loading && this.getData('loadingShow') ) {
this.loading.classList.remove('browser-loading-hide')
this.loading.classList.add('browser-loading-show')
}
},
hideLoadingRender () {
if ( this.loading && this.getData('loadingShow') ) {
this.loading.classList.remove('browser-loading-show')
this.loading.classList.add('browser-loading-hide')
}
},
clearBackTimer () {
if ( this.backTimer ) {
window.clearTimeout(this.backTimer)
this.backTimer = null
}
},
initIframe () {
this.showLoadingRender()
this.callMethod('loadstart', {href: this.iframeSrc})
this.iframe = document.createElement('IFRAME')
this.iframe.setAttribute('class', 'find_iframe_iframe')
this.iframe.setAttribute('frameborder', this.getData('frameborder'))
this.iframe.setAttribute('allow', this.getData('allow'))
this.iframe.setAttribute('allowfullscreen', this.getData('allowfullscreen'))
this.iframe.setAttribute('sandbox', this.getData('sandbox'))
this.iframe.setAttribute('crossOrigin', this.getData('crossOrigin'))
if ( this.getData('iframeStyle') ) this.iframeStyleWatcher(JSON.parse(this.getData('iframeStyle')))//初始化style
if ( this.getData('iframeClass') ) this.iframeClassWatcher(JSON.parse(this.getData('iframeClass')))//初始化style
this.iframe.src = this.iframeSrc
this.iframe.onload = () => {
this.hideLoadingRender()
const iframeWindow = this.iframe.contentWindow
iframeWindow.onbeforeunload = (e) => {
this.clearBackTimer()
this.callMethod('loadstart', e)
this.showLoadingRender()
}
const history = iframeWindow.history
const iframeDocument = this.iframe.contentDocument || iframeWindow.document;
const title = iframeDocument.title
const href = iframeWindow.location.href
const head = iframeDocument.head;
const links = head.querySelectorAll('link[rel="icon"], link[rel="shortcut icon"]');//尝试从网页代码中获取favicon.ico
const favicon = links.length > 0 ? links[0].href : href + (href.substring(href.length -1) == '/' ? '' : '/') + 'favicon.ico'//没有获取到默认使用网址拼接favicon.ico
this.callMethod('loaded', {title, href, favicon, history})
}
this.iframe.onerror = (e) => {
this.hideLoadingRender()
this.callMethod('callError', e)
}
this.dom.appendChild(this.iframe)
//控制iframe内部的console
const consoles = this.getData('console').split(' ')
Object.keys(this.iframe.contentWindow.console).forEach(key => {
if ( consoles.indexOf(key) == -1 ) this.iframe.contentWindow.console[key] = function () {}
})
const options = JSON.parse(this.getData('overrideUrlLoadingOptions'))
if ( options && options.mode ) this.overrideUrlLoadingWatcher(this.getData('overrideUrlLoadingOptions'))
},
readyWatcher (newVal) {
if ( newVal ) {
this.dom = document.querySelector('.iframe' + newVal)
this.loading = document.createElement('DIV')
this.loading.setAttribute('class', 'browser-loading-line')
this.dom.appendChild(this.loading)
}
if ( this.iframeSrc && this.dom ) this.initIframe()
},
destroyWatcher (newVal) {
if ( newVal ) this.destoryIframe()
},
srcWatcher (newVal, oldVal) {
this.iframeSrc = newVal
if ( !this.dom ) return
if ( newVal != oldVal ) {
if ( oldVal ) this.destoryIframe()
if ( newVal ) this.initIframe()
}
},
sandboxWatcher (newVal) {
this.iframe && this.iframe.setAttribute('sandbox', newVal)
},
allowWatcher (newVal) {
this.iframe && this.iframe.setAttribute('allow', allow)
},
allowfullscreenWatcher (newVal) {
this.iframe && this.iframe.setAttribute('allowfullscreen', newVal)
},
frameborderWatcher (newVal) {
this.iframe && this.iframe.setAttribute('frameborder', newVal)
},
iframeStyleWatcher (newVal) {
if ( typeof newVal == "string" ) this.iframe && this.iframe.setAttribute('style', newVal)
if ( typeof newVal == "object" ) {
let style = ''
Object.keys(newVal).forEach(key => {
style += key + ':' + newVal[key] + ';'
})
this.iframe && this.iframe.setAttribute('style', style)
}
},
iframeClassWatcher (newVal) {
if ( typeof newVal == "string" ) this.iframe && this.iframe.setAttribute('class', 'find_iframe_iframe ' + newVal)
if ( typeof newVal == "object" ) {
let className = ''
Object.keys(newVal).forEach(key => {
className += newVal[key] ? (key + ' ') : ''
})
this.iframe && this.iframe.setAttribute('class', 'find_iframe_iframe ' + className)
}
},
jumpWatcher (newVal) {
if ( newVal < 0 && this.iframe ) {
this.iframe.contentWindow.history.back()
this.backTimer = window.setTimeout(() => {
this.callMethod('backError')
}, 300)
}
},
assignUrlWatcher (newVal) {
const iframeWindow = this.iframe?.contentWindow
iframeWindow && iframeWindow.window.location.assign(newVal);
},
loadingWatcher (newVal) {
if ( newVal == 0 ) this.hideLoading()
if ( newVal == 1 ) this.showLoading()
},
evalJSWatcher (newVal) {
if ( newVal ) {
try{
const iframeDocument = this.iframe.contentDocument || iframeWindow.document;
const script = iframeDocument.createElement('SCRIPT')
script.innerHTML = newVal
iframeDocument.head.appendChild(script)//插入script标签
this.callMethod('callEvalJS', {status: 'success'})//通知逻辑层成功执行回调
iframeDocument.head.removeChild(script)//执行完script标签后需要移除
}catch(e){
this.callMethod('callEvalJS', {status: 'error', message: JSON.stringify(e)})
}
}
},
evalCSSWatcher (newVal) {
if ( newVal ) {
try{
const iframeDocument = this.iframe.contentDocument || iframeWindow.document;
const style = iframeDocument.createElement('STYLE')
style.innerHTML = newVal
iframeDocument.head.appendChild(style)
}catch(e){
this.callMethod('callError', e)
}
}
},
//给网页值传参
setDataWatcher (newVal) {
if ( newVal ) {
try{
const iframeWindow = this.iframe.contentWindow
const key = this.getData('setDataName')
if ( key ) iframeWindow[key] = newVal
}catch(e){
this.callMethod('callEvalJS', {status: 'error', message: JSON.stringify(e)})
}
}
},
//拦截监听
overrideUrlLoadingWatcher (newVal) {
// #ifdef APP-VUE
const wv = plus.webview.currentWebview()
const options = JSON.parse(newVal)
wv.overrideUrlLoading(options, (e) => {
this.callMethod('overrideUrlLoading', e)
return true;
});
// #endif
},
getData (name) {
const value = this.dom.getAttribute('data-' + name)
if ( ['true', 'false'].includes(value) ) return value == 'false' ? false : true
else if ( /^\d+$/.test(value) ) return Number(value)
else return value
},
callMethod (name, args) {
// #ifndef H5
this.$ownerInstance.callMethod(name, args)
// #endif
// #ifdef H5
this[name](args)
// #endif
}
}
}
</script>
<!-- #endif -->
<style>
@keyframes loading-show {
from {
width: 0;
}
to {
width: 99%;
}
}
@keyframes loading-hide {
from {
width: 0;
opacity: 1;
}
to {
width: 100%;
opacity: 0;
}
}
.find_iframe {
position: relative;
width: 100%;
height: 100%;
}
/deep/ .find_iframe_iframe {
width: 100%;
height: 100%;
}
/deep/ .browser-loading-line {
position: absolute;
top: 0;
left: 0;
width: 0;
height: 3px;
border-radius: 3px;
background-color: #4cd964;
z-index: 1;
touch-action: none;
pointer-events: none;
}
/deep/ .browser-loading-show {
animation: loading-show 5s ease both;
}
/deep/ .browser-loading-hide {
animation: loading-hide .5s linear both;
}
</style>

View File

@@ -0,0 +1,648 @@
<template>
<view class="render-video" :class="'rvideo' + dataId"
:data-route="pageRoute"
:dataId="dataId" :change:dataId="RenderVideo.dataIdWatcher"
:videoParams="videoParams" :change:videoParams="RenderVideo.reloadVideoRender"
:isUnload="isUnload" :change:isUnload="RenderVideo.destroyRender"
:loadDanmuParams="loadDanmuParams" :change:loadDanmuParams="RenderVideo.loadDanmuRender"
:isUnloadDanmu="isUnloadDanmu" :change:isUnloadDanmu="RenderVideo.unloadDanmuRender"
:sendDanmuParams="sendDanmuParams" :change:sendDanmuParams="RenderVideo.sendDanmuRender"
:insertDanmuParams="insertDanmuParams" :change:insertDanmuParams="RenderVideo.insertDanmuRender"
:setVideoParams="setVideoParams" :change:setVideoParams="RenderVideo.setVideoRender"
:reloadCustomParams="reloadCustomParams" :change:reloadCustomParams="RenderVideo.reloadCustomRender"
:updateConfigParams="updateConfigParams" :change:updateConfigParams="RenderVideo.updateConfigRender"
:isPlay="isPlay" :change:isPlay="RenderVideo.playRender"
:isPause="isPause" :change:isPause="RenderVideo.pauseRender"
:isToggle="isToggle" :change:isToggle="RenderVideo.toggleRender"
:seekTime="seekTime" :change:seekTime="RenderVideo.seekRender"
:isOpenFullscreen="isOpenFullscreen" :change:isOpenFullscreen="RenderVideo.openFullscreenRender"
:isExitFullscreen="isExitFullscreen" :change:isExitFullscreen="RenderVideo.exitFullscreenRender"
:captureParams="captureParams" :change:captureParams="RenderVideo.captureRender"
:toastParams="toastParams" :change:toastParams="RenderVideo.showToastRender"
:toolbarParams="toolbarParams" :change:toolbarParams="RenderVideo.showToolbarRender"
:isEnableGesture="isEnableGesture" :change:isEnableGesture="RenderVideo.enableGestureRender"
:isDisableGesture="isDisableGesture" :change:isDisableGesture="RenderVideo.disableGestureRender"
:isUpdateSize="isUpdateSize" :change:isUpdateSize="RenderVideo.updateSizeRender">
<view :class="'rvideoslot' + dataId">
<slot></slot>
</view>
</view>
</template>
<script>
export default {
data () {
return {
dataId: '',
pageRoute: '',//当前页面的路由名称
videoParams: '',//视频参数
isUnload: -1,//是否卸载视频
loadDanmuParams: '',//弹幕配置
isUnloadDanmu: -1,//是否卸载弹幕
sendDanmuParams: '',//发送弹幕
insertDanmuParams: '',//插入弹幕
setVideoParams: '',//设置video属性
reloadCustomParams: '',//重加载自定义配置
updateConfigParams: '',//更新配置
isPlay: -1,//是否播放
isPause: -1,//是否暂停
isToggle: -1,//是否切换
seekTime: -1,//跳转时间
isOpenFullscreen: -1,//是否开启全屏
isExitFullscreen: -1,//是否退出全屏
captureParams: '',//截屏
toastParams: '',//消息提示
toolbarParams: '',//工具栏
isEnableGesture: -1,//是否开启手势事件
isDisableGesture: -1,//是否关闭手势事件
isUpdateSize: -1//是否更新画布尺寸
}
},
mounted() {
this.dataId = new Date().getTime().toString() + Math.round(Math.random() * 10000)
const pages = getCurrentPages()
const page = pages[pages.length-1]
this.pageRoute = page.route
},
methods: {
ready () {
this.emit('message', {ready: true})
},
destroyed () {
this.$emit('destroyed')
},
message (e) {
this.$emit('message', e)
},
emit (name, data) {
this.$emit(name, {
data
})
},
//重加载视频
reloadVideo (params) {
this.videoParams = ''
this.$nextTick(() => {
this.videoParams = params
})
},
//卸载视频
destroy () {
this.isUnload = -1
this.$nextTick(() => {
this.isUnload = 1
})
},
//加载弹幕
loadDanmu (danmu) {
this.loadDanmuParams = ''
this.$nextTick(() => {
this.loadDanmuParams = danmu
})
},
//卸载弹幕
unloadDanmu () {
this.isUnloadDanmu = -1
this.$nextTick(() => {
this.isUnloadDanmu = 1
})
},
//发送弹幕
sendDanmu (danmu, border) {
this.sendDanmuParams = ''
this.$nextTick(() => {
this.sendDanmuParams = {
danmu,
border
}
})
},
//插入弹幕
insertDanmu (danmu) {
this.insertDanmuParams = ''
this.$nextTick(() => {
this.insertDanmuParams = danmu
})
},
//动态修改video属性
setVideo (key, value) {
this.setVideoParams = ''
this.$nextTick(() => {
this.setVideoParams = {
key, value
}
})
},
//重加载自定义配置
reloadCustom (params) {
this.reloadCustomParams = ''
this.$nextTick(() => {
this.reloadCustomParams = params
})
},
//更新配置
updateConfig (params) {
this.updateConfigParams = ''
this.$nextTick(() => {
this.updateConfigParams = params
})
},
//播放视频
play () {
this.isPlay = -1
this.$nextTick(() => {
this.isPlay = 1
})
},
//暂停视频
pause () {
this.isPause = -1
this.$nextTick(() => {
this.isPause = 1
})
},
//播放和暂停视频
toggle () {
this.isToggle = -1
this.$nextTick(() => {
this.isToggle = 1
})
},
/**
* 跳转视频
* @param {Number} time 跳转位置(单位秒)
*/
seek (time) {
this.seekTime = -1
this.$nextTick(() => {
this.seekTime = time
})
},
/**
* 播放和暂停视频
* @param {String} direction 屏幕方向 auto-自动计算 landscape-横屏 portrait-竖屏
*/
openFullscreen (direction) {
this.isOpenFullscreen = -1
this.$nextTick(() => {
this.isOpenFullscreen = direction
})
},
//退出全屏
exitFullscreen () {
this.isExitFullscreen = -1
this.$nextTick(() => {
this.isExitFullscreen = 1
})
},
//截图
capture (data) {
this.captureParams = ''
this.$nextTick(() => {
this.captureParams = data
})
},
//消息提示
showToast (data) {
this.toastParams = ''
this.$nextTick(() => {
this.toastParams = data
})
},
//展示工具栏
showToolbar (data) {
this.toolbarParams = ''
this.$nextTick(() => {
this.toolbarParams = data
})
},
//禁用手势事件
disableGesture () {
this.isDisableGesture = -1
this.$nextTick(() => {
this.isDisableGesture = 1
})
},
//启用手势事件
enableGesture () {
this.isEnableGesture = -1
this.$nextTick(() => {
this.isEnableGesture = 1
})
},
updateSize () {
this.isUpdateSize = -1
this.$nextTick(() => {
this.isUpdateSize = 1
})
}
}
}
</script>
<script lang="renderjs" module="RenderVideo" type="module">
export default {
data () {
return {
dom: null,
domSlot: null,
mp: null
}
},
beforeDestroy() {
this.destroyRender()
},
methods: {
dataIdWatcher (newVal) {
if ( newVal ) {
this.dom = document.querySelector('.rvideo' + newVal)
this.domSlot = document.querySelector('.rvideoslot' + newVal)
this.init()
}
},
async init () {
if ( !window.Hls ) await this.loadScriptInSandbox(this.parseSrc('/uni_modules/yingbing-video/static/html/js/hls.min.js'), 'hls')
if ( !window.flvjs ) await this.loadScriptInSandbox(this.parseSrc('/uni_modules/yingbing-video/static/html/js/flv.min.js'), 'flv')
if ( !window.JSMpeg ) await this.loadScriptInSandbox(this.parseSrc('/uni_modules/yingbing-video/static/html/js/jsmpeg.min.js'), 'JSMpeg')
if ( !window.THREE ) await this.loadScriptInSandbox(this.parseSrc('/uni_modules/yingbing-video/static/html/js/three.min.js'), 'three')
if ( !window.THREE.OrbitControls ) await this.loadScriptInSandbox(this.parseSrc('/uni_modules/yingbing-video/static/html/js/OrbitControls.js'), 'OrbitControls')
if ( !window.THREE.DeviceOrientationControls ) await this.loadScriptInSandbox(this.parseSrc('/uni_modules/yingbing-video/static/html/js/DeviceOrientationControls.js'), 'DeviceOrientationControls')
if ( !window.YbSubtitle ) await this.loadScriptInSandbox(this.parseSrc('/uni_modules/yingbing-video/static/html/dist/yb-player-subtitle.js'), 'ybPlayerSubtitle')
if ( !window.YbDanmu ) await this.loadScriptInSandbox(this.parseSrc('/uni_modules/yingbing-video/static/html/dist/yb-player-danmu.js'), 'ybPlayerDanmu')
if ( !window.YbPano ) await this.loadScriptInSandbox(this.parseSrc('/uni_modules/yingbing-video/static/html/dist/yb-player-pano.js'), 'ybPlayerPano')
if ( !window.YbMpeg ) await this.loadScriptInSandbox(this.parseSrc('/uni_modules/yingbing-video/static/html/dist/yb-player-mpeg.js'), 'ybPlayerMpeg')
if ( !window.YbPlayer ) await this.loadScriptInSandbox(this.parseSrc('/uni_modules/yingbing-video/static/html/dist/yb-player.js'), 'ybPlayer')
if ( !window.YbGesture ) await this.loadScriptInSandbox(this.parseSrc('/uni_modules/yingbing-video/static/html/dist/yb-player-gesture.js'), 'ybPlayerGesture')
this.loadCss()
this.callMethod('ready')
},
loadCss () {
let links = document.getElementsByTagName('link')
let linkarr = []
for ( let i = 0; i < links.length; i++ ) {
if ( links[i].getAttribute('data-id') ) linkarr.push(links[i].getAttribute('data-id'))
}
//判断一下css文件是否已加载避免重复加载
if ( linkarr.indexOf('yb-player') == -1 ) {
const link = document.createElement('LINK')
link.setAttribute('data-id', 'yb-player')
link.rel = 'stylesheet'
link.href = this.parseSrc('/uni_modules/yingbing-video/static/html/css/yb-player.css')
document.head.appendChild(link)
}
//判断一下css文件是否已加载避免重复加载
if ( linkarr.indexOf('yb-player-plugin') == -1 ) {
const link = document.createElement('LINK')
link.setAttribute('data-id', 'yb-player-plugin')
link.rel = 'stylesheet'
link.href = this.parseSrc('/uni_modules/yingbing-video/static/html/css/yb-player-plugin.css')
document.head.appendChild(link)
}
},
request (url) {
return new Promise((resolve, reject) => {
//如果是在线链接表示当前运行为H5直接用XMLHttpRequest请求
if (url.startsWith('http') ) {
// #ifdef H5
var xhr = new XMLHttpRequest()
// #endif
// #ifdef APP-VUE
var xhr = new plus.net.XMLHttpRequest()
// #endif
xhr.onreadystatechange = () => {
if ( xhr.readyState == 4 ) {
if ( xhr.status == 200 ) {
resolve(xhr.responseText)
} else {
resolve(null)
}
xhr.abort()
}
}
xhr.onabort = function () { xhr = null }
xhr.open('GET', url);
xhr.responseType = 'text';
xhr.send();
} else {
//非http协议链接需要使用系统文件API获取文件内容兼容IOS得WkWbview内核
// #ifdef APP-VUE
//将路径开头替换为_www/这是为了兼容ios文件路径读取
plus.io.resolveLocalFileSystemURL(url.replace('./', '_www/'), function( entry ) {
entry.file( function(file){
var fileReader = new plus.io.FileReader();
fileReader.readAsText(file, 'utf-8');
fileReader.onloadend = function(evt) {
resolve(evt.target.result)
}
} );
}, function ( e ) {
resolve(null)
} );
// #endif
// #ifndef APP-VUE
console.log('不支持本地文件访问')
// #endif
}
})
},
parseSrc (path) {
// #ifdef H5
const isHash = window.location.hash
const route = this.dom.getAttribute('data-route')
const pathName = isHash ? window.location.pathname : window.location.pathname.replace(route, '')
return window.location.origin + pathName + path.substring(1)
// #endif
// #ifdef APP-VUE
return '.' + path
// #endif
},
// 安全加载JS库的方法
async loadScriptInSandbox (url, id) {
try {
const response = await this.request(url)
const jsCode = response
// const uni = undefined
// eval('(function () {' + jsCode + '}())')
//创建隔离的执行环境 似乎没有效果
const sandbox = {
window: window,
document: document,
navigator: navigator
// 添加其他必要的全局对象...
};
const code = `
(function(sandbox) {
var window = sandbox.window;
var document = sandbox.document;
var navigator = sandbox.navigator;
var uni = undefined;
${jsCode}
})(sandbox);
`;
// 使用Function构造器创建隔离的执行环境
const executeInSandbox = new Function('sandbox',code);
// 执行代码
executeInSandbox(sandbox);
return { success: true, sandbox };
} catch (error) {
console.error(`加载脚本失败: ${error}`);
// return { success: false, error };
}
},
async loadScriptInDom (src, id) {
return new Promise(resolve => {
const script = document.createElement('SCRIPT')
script.setAttribute('data-id', id)
script.src = src
script.onload = function () {
resolve(true)
}
document.head.appendChild(script)
})
},
//深度克隆数据,避免数据污染
_traverseObject (obj, emitname) {
if(typeof obj !== "object" && typeof obj !== 'function') {
//原始类型直接返回
return obj;
}
var o = Object.prototype.toString.call(obj) === '[object Array]' ? [] : {};
for(let i in obj) {
if(obj.hasOwnProperty(i)){
const value = obj[i]
const emit = {}
emit[emitname] = value
o[i] = i == 'click' ? () => { this.callMethod('message', {data: emit}) } : typeof value === 'object' ? this._traverseObject(value, emitname) : value
}
}
return o;
},
//处理hlsConfig
parseHlsConfig (config = {}) {
Object.keys(config).forEach(key => {
if ( ['xhrSetup'].includes(key) ) {
config[key] = new Function('return (' + config[key] + ')')();
}
if ( ['pLoader', 'fLoader'].includes(key) ) {
config[key] = (new Function(`return ${config[key]}`))();
}
})
return config
},
reloadVideoRender (params) {
if ( !params ) return
this.unloadRender()
const custom = this._traverseObject(params.custom, 'slotclick')
this.mp = new YbPlayer({
container: this.dom,
src: params.src,
segments: params.segments,
title: params.title,
poster: params.poster || undefined,
type: params.type,
three: params.three,
initialTime: params.initialTime,
duration: params.duration,
autoplay: params.autoplay,
preload: params.preload,
muted: params.muted,
playbackRate: params.playbackRate,
loop: params.loop,
isLive: params.isLive,
header: params.header,
controls: params.controls,
height: '100%',
objectFit: params.objectFit,
crossOrigin: params.crossOrigin,
openDirection: params.openDirection,
exitDirection: params.exitDirection,
quality: params.quality,
works: params.works,
workIndex: params.workIndex,
subtitles: params.subtitles,
subtitleIndex: params.subtitleIndex,
custom,
decoder: {
hls: {
loader: Hls,
config: this.parseHlsConfig(params.hlsConfig)
},
flv: {
loader: flvjs,
config: params.flvConfig
},
jsmpeg: {
loader: JSMpeg,
config: params.jsmpegConfig
}
}
})
this.mp.load()
this.mp.loadVideo()
this.mp.loadGestureEvent()
this.mp.appendDom(this.domSlot)
this.mp.onmessage = (data) => {
this.callMethod('message', {data})
}
window.addEventListener('resize', this.updateSizeRender)
},
//动态修改video属性
setVideoRender (newVal) {
if ( !newVal ) return
const { key, value } = newVal
this.mp?.setVideo(key, value)
},
//加载弹幕
loadDanmuRender (danmu) {
if ( !danmu ) return
if ( this.mp ) {
this.mp.setConfig('danmu', YbPlayer.deepClone(danmu))
this.mp.unloadDanmu()
this.mp.loadDanmu()
}
},
//卸载弹幕
unloadDanmuRender (newVal) {
if ( newVal == -1 ) return
this.mp?.unloadDanmu()
},
//发送弹幕
sendDanmuRender (newVal) {
if ( !newVal ) return
const { danmu, border } = newVal
this.mp?.sendDanmu(danmu, border)
},
//插入弹幕
insertDanmuRender (newVal) {
if ( !newVal ) return
this.mp?.insertDanmu(newVal)
},
//更新配置
updateConfigRender (config) {
if ( !config ) return
Object.keys(config).forEach(key => {
this.mp?.setConfig(key, config[key])
})
this.mp?.hideControls()
},
//重加载自定义配置
reloadCustomRender (config) {
if ( !config ) return
const newConfig = this._traverseObject(config, 'slotclick')
Object.keys(newConfig).forEach(key => {
this.mp?.setCustom(key, newConfig[key])
})
this.mp?.unloadCustom()
this.mp?.loadCustom()
},
//播放/暂停
toggleRender (newVal) {
if ( newVal == -1 ) return
this.mp?.toggle()
},
//播放
playRender (newVal) {
if ( newVal == -1 ) return
this.mp?.video?.play()
},
//暂停
pauseRender (newVal) {
if ( newVal == -1 ) return
this.mp?.video?.pause()
},
//跳转
seekRender (time) {
if ( time == -1 ) return
this.mp?.seek(time)
},
//开启全屏
openFullscreenRender (direction) {
if ( direction == -1 ) return
this.mp?.openFullscreen(direction)
},
//退出全屏
exitFullscreenRender (newVal) {
if ( newVal == -1 ) return
this.mp?.exitFullscreen()
},
//消息提示
showToastRender (data) {
if ( !data ) return
this.mp?.showToast(data)
},
//展示工具栏
showToolbarRender (data) {
if ( !data ) return
const newDate = this._traverseObject(data, 'toolclick')
this.mp?.showToolbar(newDate.selector, newDate.list, newDate.checkShow, newDate.checkIndex)
},
//截图
captureRender (data) {
if ( !data ) return
this.mp?.capture(data.type, data.show)
},
//禁用手势事件
disableGestureRender (newVal) {
if ( newVal == -1 ) return
this.mp?.disableGesture()
},
//启用手势事件
enableGestureRender (newVal) {
if ( newVal == -1 ) return
this.mp?.enableGesture()
},
//卸载视频
unloadRender () {
if ( this.mp ) {
if ( this.domSlot && this.dom ) this.dom.appendChild(this.domSlot)
this.mp.unloadDanmu()
this.mp.unloadGestureEvent()
this.mp.unloadVideo()
this.mp.unload()
window.removeEventListener('resize', this.updateSizeRender)
}
},
//卸载视频
destroyRender (newVal) {
if ( newVal == 1 ){
this.unloadRender()
this.callMethod('destroyed')
}
},
//重置画布尺寸
updateSizeRender () {
//重置弹幕画布
this.mp?.refreshDanmu()
//重置3D画布
this.mp?.refreshPano()
},
callMethod (name, args) {
// #ifndef H5
this.$ownerInstance && this.$ownerInstance.callMethod(name, args)
// #endif
// #ifdef H5
this[name] && this[name](args)
// #endif
}
}
}
</script>
<style>
.render-video {
background-color: #000;
width: 100%;
height: 100%;
}
.render-video-slot {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,828 @@
<template>
<view class="yingbing-video" :class="{'yingbing-video-full': fullscreen}" :style="[boxStyle]">
<!-- #ifdef H5 || APP-VUE -->
<!-- 根据平台选择加载方式因为IOS使用WKWebview内核时严格执行同源策略对于iframe加载本地网页以及非同源线上网页都有很大的限制无法操作iframe内容 -->
<r-video v-if="renderType == 'renderjs'" ref="web" class="yb-iframe" @message="handleMessage" @destroyed='handleDestroy'>
<slot></slot>
</r-video>
<r-iframe v-else ref="web" class="yb-iframe"
crossOrigin="anonymous"
:src="webSrc" @message="handleMessage" :loading-show="false" @destroyed='handleDestroy'></r-iframe>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<web-view style="flex: 1;" ref="web" :src="webSrc" @onPostMessage="handleMessage"></web-view>
<!-- #endif -->
</view>
</template>
<script>
// #ifdef APP-VUE || H5
import RIframe from '../modules/iframe.vue'
import RVideo from '../modules/video.vue'
// #endif
export default {
// #ifdef APP-VUE || H5
components: {RIframe, RVideo},
// #endif
props: {
//iframe或者webview访问的自定义src
webSrc: {
type: String,
default: () => {
const path = '/uni_modules/yingbing-video/static/html/video.html'
// #ifdef H5
const isHash = window.location.hash
const pages = getCurrentPages()
const page = pages[pages.length-1]
const pathName = isHash ? window.location.pathname : window.location.pathname.replace(page.route, '')
return window.location.origin + pathName + path.substring(1)
// #endif
// #ifdef APP-VUE
return '.' + path
// #endif
// #ifdef APP-NVUE
return path
// #endif
}
},
//播放链接
src: {
type: String,
default: ''
},
//切片列表
segments: {
type: Array,
default: () => []
},
//封面
poster: {
type: String,
default: ''
},
//标题
title: {
type: String,
default: ''
},
//解码类型 auto-自动判断 hls-使用hls.js加载 flv-使用flv.js加载 video-使用video加载
type: {
type: String,
default: 'auto'
},
//初始化时间
initialTime: {
type: Number,
default: 0
},
//自定义最大播放时长
duration: {
type: Number,
default: 0
},
//3D模式 none-关闭3D 360-360全景
three: {
type: String,
default: 'none'
},
//自动播放(不保证能执行)
autoplay: {
type: Boolean,
default: false
},
//预加载 auto-浏览器尽可能地预加载整个视频文件 metadata-仅预加载视频的元数据 none-不预加载视频
preload: {
type: String,
default: 'auto'
},
//静音
muted: {
type: Boolean,
default: false
},
//播放倍速
playbackRate: {
type: Number,
default: 1
},
//循环播放
loop: {
type: Boolean,
default: false
},
//直播流
isLive: {
type: Boolean,
default: false
},
//非全屏时是否显示头部控制栏
header: {
type: Boolean,
default: false
},
//是否显示底部控制栏
controls: {
type: Boolean,
default: false
},
//视频组件高度 auto-自适应高度 百分比-根据屏幕高度计算 px/rpx-像素高度
height: {
type: String,
default: 'auto'
},
//当视频宽高超出容器时的表现形式 fill-内容拉伸填充 contain-保持比例内容缩放 cover-保持比例内容可能被剪切 none-内容不重置 scale-down-保持比例从none或contain选一个 initial-默认值
objectFit: {
type: String,
default: 'contain'
},
//跨域属性 anonymous-它有一个默认值。它定义了将在不传递凭据信息的情况下发送CORS请求 use-credentials-将发送带有凭据、cookie 和证书的 cross-origin 请求
crossOrigin: {
type: String,
default: ''
},
//开启全屏时的屏幕方向
openDirection: {
type: String,
default: ''
},
//退出全屏时的屏幕方向
exitDirection: {
type: String,
default: ''
},
//弹幕列表
danmu: {
type: Array,
default: () => []
},
//画质列表
quality: {
type: Array,
default: () => []
},
//分p列表
works: {
type: Array,
default: () => []
},
//分P默认索引
workIndex: {
type: Number,
default: -1
},
//字幕列表
subtitles: {
type: Array,
default: () => []
},
//字幕默认索引
subtitleIndex: {
type: Number,
default: -1
},
//自定义配置
custom: {
type: Object,
default: () => ({})
},
//hls配置
hlsConfig: {
type: Object,
default: () => ({})
},
//flv配置
flvConfig: {
type: Object,
default: () => ({})
},
//jsmpeg配置
jsmpegConfig: {
type: Object,
default: () => ({})
},
//渲染类型 APP-NVUE只能使用web网页加载得方式渲染
// #ifdef APP-NVUE
renderType: {
type: String,
default: 'web'
},
// #endif
//渲染类型
// #ifdef APP-VUE || H5
renderType: {
type: String,
default: () => {
//如果是ios平台默认使用renderjs方式渲染
if ( uni.getSystemInfoSync().platform == 'ios' ) {
return 'renderjs'
} else {
return 'web'
}
}
}
// #endif
},
computed: {
boxStyle () {
return this.height == 'inherit' ? {
height: this.boxHeight,
flex: 1
} : {
height: this.boxHeight
}
}
},
data () {
return {
ready: false,
boxHeight: '',
videoWidth: 0,
videoHeight: 0,
fullscreen: false,
customHandles: {},//自定义处理方法
slotHandles: {},//存储插槽中的点击函数
toolHandles: {}//存储工具栏中的点击函数
}
},
beforeDestroy() {
this._removeBackbuttonListener()
},
created() {
//获取当前运行的平台
this.updateHeight()
},
methods: {
//接收消息
handleMessage (e) {
// #ifdef APP-NVUE
e.detail.data.forEach(item => {
this.parseMessage(item)
})
// #endif
// #ifdef H5 || APP-VUE
//使用uni.postMessage在h5传递消息数据结构有很大不同需要找到对应数据
this.parseMessage(e.data.type == 'WEB_INVOKE_APPSERVICE' ? e.data.data.arg : e.data)
// #endif
},
//处理返回的消息
async parseMessage (data) {
//加载视频
if ( data.ready ) {
this.ready = true
if ( this.src || (this.segments && this.segments.length) ) {
this.reloadVideo()
}
}
//视频销毁
if ( data.destroyed ) {
if ( this.renderType != 'renderjs' ) this.$refs.web && this.$refs.web.destroy && this.$refs.web.destroy()
}
//记录视频尺寸,更新视频高度
if ( data.loadeddata ) {
this.videoWidth = data.loadeddata.videoWidth
this.videoHeight = data.loadeddata.videoHeight
this.updateHeight()
}
//用户点击了插槽内容,触发了点击函数
if ( data.slotclick ) {
this.slotHandles[data.slotclick] && this.slotHandles[data.slotclick]()//触发存储的函数
}
//用户点击了工具栏,触发了点击函数
if ( data.toolclick ) {
this.toolHandles[data.toolclick] && this.toolHandles[data.toolclick]()//触发存储的函数
}
// #ifdef APP-PLUS
//用户在非5+环境点击了保存截图
if ( data.capturesaved ) {
if ( data.capturesaved.code != 200 ) {
this.saveBase64ImageToAlbum(data.capturesaved.data.base64, data.capturesaved.fileName).then(path => {
this.showToast('保存到相册成功')
}).catch(() => {
this.showToast('保存到相册失败')
})
}
}
//设置亮度
if ( data.seeklight >= 0 ) {
if ( this.customHandles.setLight ) {
this.customHandles.setLight(data.seeklight)
} else {
uni.setScreenBrightness({
value: data.seeklight
})
}
}
//设置音量
if ( data.seekvolume >= 0 ) {
if ( this.customHandles.setVolume ) {
this.customHandles.setVolume(data.seekvolume)
} else {
plus.device.setVolume(data.seekvolume)
}
}
//方向改变监听
if ( data.directionchange ) {
if ( data.directionchange.code != 200 ) {//锁定屏幕方向失败
var direction = data.directionchange.to//预期屏幕方向
if ( direction ) plus.screen.lockOrientation(direction)//锁定屏幕方向
else plus.screen.unlockOrientation()//解除屏幕方向锁定
}
}
//APP全屏改变监听
if ( data.fullscreenchange ) {
if ( data.fullscreenchange.type == 'css' ) {//使用css模拟全屏
this.fullscreen = data.fullscreenchange.fullscreen
if ( this.fullscreen ) {
plus.navigator.setFullscreen(true)//调用系统全屏
plus.navigator.hideSystemNavigation()//隐藏系统导航栏
} else {
plus.navigator.setFullscreen(false)//退出系统全屏
setTimeout(function () {//不延迟,无法重新显示导航栏
plus.navigator.showSystemNavigation()//隐藏系统导航栏
}, 200)
}
if ( this.fullscreen ) {
this._bindbackbutton = this.exitFullscreen.bind(this)
plus.key.addEventListener("backbutton",this._bindbackbutton);//增加返回键监听,用于点击返回退出全屏
} else {
this._removeBackbuttonListener()//移除返回键监听事件
}
}
if( !data.fullscreenchange.fullscreen ) this.updateHeight()//考虑到全屏时可能会加载新视频,退出全屏需要重新更新高度
}
// #endif
// #ifdef H5
//H5全屏改变监听
if ( data.fullscreenchange ) {
if ( data.fullscreenchange.type == 'css' ) {//使用css模拟全屏
this.fullscreen = data.fullscreenchange.fullscreen
}
if ( !data.fullscreenchange.fullscreen ) this.updateHeight() //考虑到全屏时可能会加载新视频,退出全屏需要重新更新高度
}
// #endif
//派发事件
Object.keys(data).forEach(key => {
if ( key != 'destroyed' ) this.$emit(key, data[key])
})
},
//移除返回键监听
_removeBackbuttonListener () {
if ( this._bindbackbutton ) {
plus.key.removeEventListener("backbutton",this._bindbackbutton);
this._bindbackbutton = null
}
},
//处理custom中的函数
_traverseObject(obj, handleName) {
if (typeof obj !== 'object' || obj === null) {
return;
}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
if (typeof value === 'function') {
const id = new Date().getTime().toString() + Math.round(Math.random() * 10000)
this[handleName][id] = value
obj[key] = id
} else if (typeof value === 'object' && value !== null) {
this._traverseObject(value, handleName);
}
}
}
},
//深度克隆数据,避免数据污染
_deepClone (obj) {
if(typeof obj !== "object" && typeof obj !== 'function') {
//原始类型直接返回
return obj;
}
var o = Object.prototype.toString.call(obj) === '[object Array]' ? [] : {};
for(let i in obj) {
if(obj.hasOwnProperty(i)){
o[i] = i === 'loader' ? '' : typeof obj[i] === "object" ? this._deepClone(obj[i]) : obj[i];
}
}
return o;
},
//更新高度
updateHeight () {
if ( this.updateTimer ) {
clearTimeout(this.updateTimer)
this.updateTimer = null
}
this.updateTimer = setTimeout(() => {
//计算自适应高度
if ( this.height == 'auto' && this.videoWidth ) {
const windowWidth = uni.getSystemInfoSync().windowWidth
const rate = windowWidth / this.videoWidth
this.boxHeight = (this.videoHeight * rate) + 'px'
}
//继承父级高度
if ( this.height == 'inherit' ) {
this.boxHeight = '100%'
}
//像素高度
if ( this.height.includes('px') ) {
this.boxHeight = this.height
}
//计算百分比高度
if ( this.height.includes('%') ) {
const windowHeight = uni.getSystemInfoSync().windowHeight
this.boxHeight = (windowHeight * (this.height.replace('%', '') / 100)) + 'px'
}
//去更新视频内部画布的尺寸
this.$nextTick(() => {
this.evalJS('updateSize')
})
}, 100)
},
//处理自定义配置
async parseCustom () {
const custom = this._deepClone(this.custom)
//APP端设置初始化亮度和音量值
// #ifdef APP-PLUS
const initialLight = await this.getLight()
const initialVolume = await this.getVolume()
const gesture = custom.gesture || {}
gesture.initialLight = gesture.initialLight || initialLight
gesture.initialVolume = gesture.initialVolume || initialVolume
custom.gesture = gesture
// #endif
//H5端不能通过手势操作亮度和音量
// #ifdef H5
const gesture = custom.gesture || {}
gesture.disableLight = true
gesture.disableVolume = true
custom.gesture = gesture
// #endif
//尝试获取自定义设置音量和亮度的方法
this.customHandles.setLight = gesture.setLight
this.customHandles.setVolume = gesture.setVolume
this.customHandles.getLight = gesture.getLight
this.customHandles.getVolume = gesture.getVolume
gesture.setLight = null
gesture.setVolume = null
gesture.getLight = null
gesture.getVolume = null
//重置插槽函数
this.slotHandles = {}
this._traverseObject(custom, 'slotHandles')
return custom
},
//处理文件链接(针对本地链接)
parseSrc (src) {
//本地static文件夹下的文件需要单独处理路径否则无法加载
if ( src.startsWith('/static') ) {
// #ifdef H5
const isHash = window.location.hash
const pages = getCurrentPages()
const page = pages[pages.length-1]
const pathName = isHash ? window.location.pathname : window.location.pathname.replace(page.route, '')
return window.location.origin + pathName + src.substring(1)
// #endif
// #ifdef APP-PLUS
return plus.io.convertLocalFileSystemURL(src)
// #endif
}
return src
},
//处理工具栏配置
parseTool (data) {
this.toolHandles = {}
this._traverseObject(data, 'toolHandles')
return data
},
//处理字幕|画质的链接
parseList (data) {
return data.map(item => {
return {...item, src: this.parseSrc(item.src)}
})
},
//处理切片列表链接
parseSegments (data) {
return data.map(item => {
return {...item, url: this.parseSrc(item.url)}
})
},
//处理通信参数
parseArg (arg) {
//必须转义2次否则通信时可能会丢失字符
return encodeURIComponent(encodeURIComponent(JSON.stringify(arg)))
},
//重加载视频
async reloadVideo () {
const arg = {
src: this.parseSrc(this.src),
segments: this.parseSegments(this.segments),
title: this.title,
poster: this.poster,
type: this.type,
three: this.three,
initialTime: this.initialTime,
duration: this.duration,
autoplay: this.autoplay,
preload: this.preload,
muted: this.muted,
loop: this.loop,
playbackRate: this.playbackRate,
isLive: this.isLive,
header: this.header,
controls: this.controls,
objectFit: this.objectFit,
crossOrigin: this.crossOrigin,
openDirection: this.openDirection,
exitDirection: this.exitDirection,
quality: this.parseList(this.quality),
works: this.works,
workIndex: this.workIndex,
subtitles: this.parseList(this.subtitles),
subtitleIndex: this.subtitleIndex,
custom: await this.parseCustom(),
flvConfig: this.flvConfig,
hlsConfig: this.hlsConfig,
jsmpegConfig: this.jsmpegConfig
}
this.evalJS('reloadVideo', arg)
},
//卸载视频
unload () {
this.evalJS('destroy')
},
//加载弹幕
loadDanmu () {
// #ifdef APP-VUE || H5
if ( this.renderType == 'renderjs' ) {
this.evalJS('loadDanmu', this.danmu)
} else {
this.$refs.web && this.$refs.web.setData('danmu', this.danmu)
setTimeout(() => {
this.evalJS('loadDanmu')
}, 100)
}
// #endif
// #ifdef APP-NVUE
const size = 100//分成100组传入不然可能会超过字符串最大传输限制
const list = this.danmu || []
const len = Math.ceil(list.length / size)
for ( let i = 0 ; i < len; i++ ) {
const arr = list.slice(i * size, (i + 1) * size)
const arg = {code: i < len - 1 ? 0 : 1, data: arr}
this.evalJS('setDanmuData', arg)
}
// #endif
},
//卸载弹幕
unloadDanmu () {
this.evalJS('unloadDanmu')
},
//发送弹幕
sendDanmu (danmu, border) {
this.evalJS('sendDanmu', danmu, border)
},
//插入弹幕
insertDanmu (danmu) {
this.evalJS('insertDanmu', danmu)
},
//动态修改video属性
setVideo (key, value) {
this.evalJS('setVideo', key, value)
},
//重加载自定义配置
reloadCustom () {
if ( this.customTimer ) {
clearTimeout(this.customTimer)
this.this.customTimer = null
}
this.customTimer = setTimeout(async () => {
const custom = await this.parseCustom()
this.evalJS('reloadCustom', custom)
}, 200)
},
//更新配置
updateConfig () {
if ( this.updateTimer ) {
clearTimeout(this.updateTimer)
this.updateTimer = null
}
this.updateTimer = setTimeout(() => {
const arg = {
header: this.header,
controls: this.controls
}
this.evalJS('updateConfig', arg)
}, 200)
},
//播放视频
play () {
this.evalJS('play')
},
//暂停视频
pause () {
this.evalJS('pause')
},
//播放和暂停视频
toggle () {
this.evalJS('toggle')
},
/**
* 跳转视频
* @param {Number} time 跳转位置(单位秒)
*/
seek (time) {
this.evalJS('seek', time)
},
/**
* 开启视频全屏
* @param {String} direction 屏幕方向 auto-自动计算 landscape-横屏 portrait-竖屏
*/
openFullscreen (direction) {
this.evalJS('openFullscreen', direction)
},
//退出全屏
exitFullscreen () {
this.evalJS('exitFullscreen')
},
/**
* 截图
* @param {String} type video-使用video标签截图 3D-3D模式下截图渲染的canvas
* @param {String} show 是否展示截图弹窗
*/
capture (type, show = true) {
this.evalJS('capture', {type, show})
},
//消息提示
showToast (data) {
this.evalJS('showToast', data)
},
//展示工具栏
showToolbar (data) {
if ( typeof data == 'object' ) data = this.parseTool(data)
this.evalJS('showToolbar', data)
},
//禁用手势事件
disableGesture () {
this.evalJS('disableGesture')
},
//启用手势事件
enableGesture () {
this.evalJS('enableGesture')
},
//获取屏幕亮度
async getLight () {
//是否传入了自定义获取亮度方法
if ( this.customHandles.getLight ) {
return await this.customHandles.getLight()
} else {
return new Promise(resolve => {
uni.getScreenBrightness({
success: res => {
resolve(res.value)
}
})
})
}
},
//获取设备音量
getVolume () {
//是否传入了自定义获取音量方法
if ( this.customHandles.getVolume ) {
return this.customHandles.getVolume()
} else {
return plus.device.getVolume()
}
},
//监听组件销毁
handleDestroy () {
this.$emit('destroyed')
},
//保存base64图片到相册
saveBase64ImageToAlbum (base64, fileName) {
return new Promise((resolve, reject) => {
const basePath = '_doc'
const dirPath = 'uniapp_temp'
const tempFilePath = basePath + '/' + dirPath + '/' + fileName
const bitmap = new plus.nativeObj.Bitmap(fileName)
bitmap.loadBase64Data(base64, function() {
bitmap.save(tempFilePath, {}, function() {
bitmap.clear()
plus.gallery.save(tempFilePath, function (e) {
resolve(e.path)
}, function (error) {
reject(error)
});
}, function(error) {
bitmap.clear()
reject(error)
})
}, function(error) {
bitmap.clear()
reject(error)
})
})
},
//注入js根据平台不同调用方式不同
evalJS (name, ...args) {
//生成注入js函数名
let functionName = name + '('
//拼接参数
args.forEach(arg => {
if ( typeof arg == 'object' ) {
//是对象需要转义并加上双引号
functionName += '"' + this.parseArg(arg) + '",'
} else if ( typeof arg == 'string' ) {
//字符串加上双引号
functionName += '"' + arg + '",'
} else {
//其它类型直接拼接
functionName += arg + ','
}
})
//去掉最后一个逗号
const lastIndex = functionName.lastIndexOf(',')
if ( lastIndex > -1 ) functionName = functionName.substr(0, lastIndex)
//收尾括号
functionName += ');'
// #ifdef APP-VUE || H5
//如果时renderjs渲染方式直接调用内部方法
if ( this.renderType == 'renderjs' ) this.$refs.web && this.$refs.web[name](...args)
//iframe渲染注入js函数
else this.$refs.web && this.$refs.web.evalJS(functionName)
// #endif
// #ifdef APP-NVUE
//webview渲染注入js函数
this.$refs.web && this.$refs.web.evalJS(functionName)
// #endif
}
},
watch: {
//监听播放链接
src (newVal) {
if ( newVal && this.ready ) {
this.reloadVideo()
}
},
//监听切片列表
segments: {
handler(newVal, oldVal) {
//src播放链接权重比切片列表高传入播放链接的情况下不监听segments
if ( !this.src ) this.reloadVideo()
},
deep: true
},
//监听静音属性
muted (newVal) {
this.setVideo('muted', newVal)
},
//监听倍速属性
playbackRate (newVal) {
this.setVideo('playbackRate', newVal)
},
//监听循环属性
loop (newVal) {
this.setVideo('loop', newVal)
},
//监听高度
height (newVal) {
this.updateHeight()
},
//监听header
header () {
this.updateConfig()
},
//监听controls
controls () {
this.updateConfig()
},
//深度监听custom
custom: {
handler(newVal, oldVal) {
this.reloadCustom()
},
deep: true
}
}
}
</script>
<style>
/* #ifndef APP-NVUE */
.yingbing-video {
width: 100%;
}
.yb-iframe {
width: 100%;
height: 100%;
}
/* #endif */
.yingbing-video-full {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100vw!important;
height: 100vh!important;
z-index: 9999999;
}
</style>

View File

@@ -0,0 +1,8 @@
<template>
</template>
<script>
</script>
<style>
</style>

View File

@@ -0,0 +1,78 @@
{
"id": "yingbing-video",
"displayName": "好用视频播放器",
"version": "1.8.2",
"description": "支持FLV、HLS、JSMPEG、360全景、字幕、弹幕、画质切换的播放器",
"keywords": [
"视频",
"播放器"
],
"repository": "https://gitee.com/yingbing-developer/yingbing-video.git",
"engines": {
"HBuilderX": "^3.99"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"type": "component-vue"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "n",
"IE": "n",
"Edge": "n",
"Firefox": "n",
"Safari": "n"
},
"小程序": {
"微信": "n",
"阿里": "n",
"百度": "n",
"字节跳动": "n",
"QQ": "n"
},
"快应用": {
"华为": "n",
"联盟": "n"
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,102 @@
/*手势事件*/
.yb-player-center-value {
font-size: 18px;
padding: 20px 10px;
border-radius: 5px;
background-color: rgba(0, 0, 0, 0.5);
color: #fff;
}
.yb-player-rate {
position: absolute;
top: 10%;
left: 50%;
padding: 5px 15px;
border-radius: 5px;
background-color: rgba(0, 0, 0, 0.5);
color: #fff;
font-size: 14px;
transform: translateX(-50%);
display: flex;
align-items: center;
}
.yb-player-rate-icon {
display: flex;
align-items: center;
}
.yb-player-rate-icon i {
width: 0;
height: 0;
border-left: 10px solid #fff;
border-right: 10px solid transparent;
border-bottom: 5px solid transparent;
border-top: 5px solid transparent;
animation: arrowMove 1s infinite linear;
margin-right: -5px;
}
.yb-player-rate-icon i:nth-child(0) {
animation-delay: 0s;
}
.yb-player-rate-icon i:nth-child(1) {
animation-delay: 0.3s;
}
.yb-player-rate-icon i:nth-child(2){
animation-delay: 0.5s;
}
@keyframes arrowMove {
0%, 100% {
opacity: 0.2;
}
50% {
opacity: 1;
}
}
/*字幕*/
.yb-player-subtitle-text {
position: absolute;
left: 0;
width: 100%;
box-sizing: border-box;
display: flex;
justify-content: center;
padding: 0 5%;
}
.yb-player-subtitle-loading {
position:absolute;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0,0,0,.5);
color:#fff;bottom: 50px;
font-size: 18px;
padding: 5px 20px;
}
/*弹幕过滤*/
.yb-player-filter-item {
display: flex;
align-items: center;
margin-top: 5px;
}
.yb-player-filter-item-type {
padding: 2px 5px;
background-color: var(--color-warning);
font-size: 12px;
color: #fff;
margin-right: 10px;
border-radius: 5px;
}
.yb-player-filter-item-content {
font-size: 14px;
color: #eee;
flex: 1;
}
.yb-player-filter-item button {
background-color: var(--color-error);
font-size: 14px;
color: #fff;
border: none;
}

View File

@@ -0,0 +1,810 @@
:root {
--color-primary: #007aff;
--color-success: #4cd964;
--color-warning: #f0ad4e;
--color-error: #dd524d;
--safe-area-default-gap: 15px;
--safe-area-default-top-gap: 10px;
--safe-area-default-bottom-gap: 10px;
--safe-area-inset-top: var(--safe-area-default-top-gap);
--safe-area-inset-right: var(--safe-area-default-gap);
--safe-area-inset-bottom: var(--safe-area-default-bottom-gap);
--safe-area-inset-left: var(--safe-area-default-gap);
@supports (top: constant(safe-area-inset-top)) {
--safe-area-inset-top: max(var(--safe-area-default-top-gap), constant(safe-area-inset-top));
--safe-area-inset-right: max(var(--safe-area-default-gap), constant(safe-area-inset-right));
--safe-area-inset-bottom: max(var(--safe-area-default-bottom-gap), constant(safe-area-inset-bottom));
--safe-area-inset-left: max(var(--safe-area-default-gap), constant(safe-area-inset-left));
}
@supports (top: env(safe-area-inset-top)) {
--safe-area-inset-top: max(var(--safe-area-default-top-gap), env(safe-area-inset-top));
--safe-area-inset-right: max(var(--safe-area-default-gap), env(safe-area-inset-right));
--safe-area-inset-bottom: max(var(--safe-area-default-bottom-gap), env(safe-area-inset-bottom));
--safe-area-inset-left: max(var(--safe-area-default-gap), env(safe-area-inset-left));
}
}
.yb-player-wrapper {
position: relative;
overflow: hidden;
width: 100%;
height: 100%;
box-sizing: border-box;
}
/* 模拟全屏样式 */
.yb-player-openfull {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100vw;
height: 100vh;
z-index: 9999999;
}
.yb-player-danmu {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
}
.yb-player-bottom-progress {
position: absolute;
bottom: 0;
left: 0;
height: 1px;
background-color: var(--color-error);
width: 0;
}
.yb-player-controls {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 2;
padding: 10px 15px;
transform: translateY(100%);
-webkit-transform: translateY(100%);
transition: transform .3s;
color: #fff;
font-size: 15px;
}
.yb-player-controls-show {
transform: translateY(0);
-webkit-transform: translateY(0);
}
.yb-player-controls svg {
width: 25px;
height: 25px;
fill: #fff;
}
.yb-player-progress {
display: flex;
flex-direction: row;
align-items: center;
}
.yb-player-progress > * {
margin-left: 5px;
}
.yb-player-progress > :first-child {
margin-left: 0;
}
.yb-player-progress span {
color: #fff;
font-size: 14px;
}
.yb-player-progress-center {
display: flex;
align-items: center;
flex: 1;
}
.yb-player-progress-toggle {
margin-right: 10px;
}
.yb-player-progress-fullscreen {
margin-left: 10px;
}
.yb-player-live {
margin-right: 5px;
}
.yb-player-range-box {
width: 0;
flex: 1;
position: relative;
height: 2px;
margin: 0 10px;
}
.yb-player-range-track {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #999;
}
.yb-player-range-focus, .yb-player-range-preload {
position: absolute;
top: 0;
left: 0;
bottom: 0;
background-color: #fff;
width: 0;
}
.yb-player-range-preload {
background-color: #ccc;
}
.yb-player-range-thumb {
position: absolute;
top: 50%;
left: 0;
transform: translate(-50%, -50%);
width: 15px;
height: 15px;
border-radius: 15px;
background-color: #fff;
}
.yb-player-range:focus {
outline: none;
}
.yb-player-range {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: none;
outline: none;
width: 100%;
height: 2px;
margin: 0;
position: absolute;
top: 0;
bottom: 0;
left: 0;
}
.yb-player-range::-webkit-slider-thumb {
-webkit-appearance: none;
height: 14px;
width: 14px;
border-radius: 14px;
background: #fff;
}
.yb-player-range::-moz-range-thumb {
height: 7px;
width: 7px;
border-radius: 8px;
background: #fff;
}
.yb-player-range::-ms-thumb {
height: 7px;
width: 7px;
border-radius: 8px;
background: #fff;
}
.yb-player-controls-bottom {
display: flex;
flex-direction: row;
align-items: center;
}
.yb-player-controls-bottom > * {
margin-left: 5px;
}
.yb-player-controls-bottom > :first-child {
margin-left: 0;
}
.yb-player-toggle {
margin-left: 0;
}
.yb-player-toggle svg {
width: 18px;
height: 18px;
}
.yb-player-next {
margin-left: 0;
}
.yb-player-danmu-send {
flex: 1;
width: 0;
box-sizing: border-box;
padding: 5px 10px;
border-radius: 5px;
background-color: #000;
color: #999;
font-size: 14px;
}
.yb-player-header {
position: absolute;
top: 0;
left: 0;
right: 0;
padding: 10px 15px;
background-color: rgba(0,0,0,0.5);
align-items: center;
z-index: 2;
display: flex;
color: #fff;
font-size: 14px;
transform: translateY(-100%);
transition: transform .3s;
}
.yb-player-header-show {
transform: translateY(0);
}
.yb-player-title {
color: #fff;
font-size: 14px;
margin-top: -2px;
flex: 1;
word-break: break-all;
}
.yb-player-header svg {
width: 25px;
height: 25px;
fill: #fff;
}
.yb-player-header > * {
margin-left: 10px;
}
.yb-player-header > *:first-child {
margin-left: 0;
}
.yb-player-error {
display: flex;
flex-direction: column;
align-items: center;
max-width: 80%;
}
.yb-player-error-btns {
display: flex;
align-items: center;
margin-top: 10px;
}
.yb-player-error-btn {
padding: 10px 20px;
color: #fff;
font-size: 14px;
border-radius: 5px;
}
.yb-player-error-close {
background-color: var(--color-error);
}
.yb-player-error svg {
fill: #fff;
stroke: #fff;
width: 30px!important;
height: 30px!important;
}
.yb-player-error span {
font-size: 14px!important;
color: #fff;
margin-top: 5px;
}
.yb-player-toast {
position: absolute;
bottom: 10%;
left: 50%;
transform: translate(-50%, 200px);
width: 80%;
display: flex;
justify-content: center;
z-index: 4;
}
.yb-player-toast-message {
background-color: rgba(0, 0, 0, 0.5);
color: #fff;
font-size: 14px;
padding: 5px 15px;
border-radius: 5px;
}
.yb-player-toast-show {
animation: show-toast 300ms both;
}
.yb-player-toast-hide {
animation: hide-toast 300ms both;
}
.yb-player-capture-popup {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80%;
height: 80%;
z-index: 1;
animation: capture 0.1s ease-in-out both;
box-sizing: border-box;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
background-color:#fff;
padding: 5px;
display: flex;
flex-direction: column;
}
.yb-player-capture-image {
width: 100%;
flex: 1;
height: 0;
flex-shrink: 0;
}
.yb-player-capture-btns {
display: flex;
align-items: center;
padding: 10px 0 5px 0;
}
.yb-player-capture-btn {
flex: 1;
color: #333;
text-align: center;
}
.yb-player-capture-btn:first-child {
border-right: 1px solid #999;
}
.yb-player-capture-save {
color: var(--color-error);
}
.yb-player-lock {
background-color: rgba(0, 0, 0, 0.5);
width: 50px;
height: 50px;
border-radius: 50px;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 2;
}
.yb-player-lock svg {
width: 20px;
height: 20px;
fill: #fff;
}
.yb-player-lock-left {
left: var(--safe-area-inset-left);
}
.yb-player-lock-right {
right: var(--safe-area-inset-right);
}
/* 设置弹窗 */
.yb-player-setting-line {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 16px;
box-sizing: border-box;
width: 100%;
margin-top: 15px;
color: #fff;
}
.yb-player-setting-label {
flex-shrink: 0;
margin-right: 20px;
}
.yb-player-setting-right {
display: flex;
align-items: center;
}
.yb-player-setting-mini-btn {
border: none;
background-color: #fff;
color: #333;
padding: 5px 10px;
}
.yb-player-setting-reduce {
width: 20px;
height: 3px;
background-color: #fff;
}
.yb-player-setting-add {
width: 20px;
height: 3px;
background-color: #fff;
position: relative;
}
.yb-player-setting-add::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 3px;
height: 20px;
background-color: #fff;
}
.yb-player-setting-switch {
width: 50px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.yb-player-setting-switch-active {
background-color: var(--color-error);
border-radius: 30px;
}
.yb-player-setting-input {
color: #fff;
border: none;
border-bottom: 1px solid #fff;
background: none;
font-size: 14px;
flex: 1;
width: 0;
padding: 0;
margin: 0;
outline: none;
min-height: 35px;
box-sizing: border-box;
}
.yb-player-quality-box {
width: 50%;
}
.yb-player-setting-danmu-speed,
.yb-player-setting-danmu-opacity-percent,
.yb-player-setting-danmu-size,
.yb-player-setting-danmu-diffrence {
width: 100px;
text-align: center;
}
.yb-player-danmu-send-color::-webkit-color-swatch-wrapper {
padding: 0;
}
.yb-player-danmu-send-color::-webkit-color-swatch {
border: 0;
}
.yb-player-danmu-send-textarea::placeholder {
color: #999;
}
.yb-player-danmu-send-btn {
padding: 8px 20px;
text-align: center;
background-color: var(--color-error);
color: #fff;
font-size: 18px;
border-radius: 5px;
margin-top: 10px;
}
.yb-player-work-item {
padding: 10px;
border: 1px solid #fff;
color: #fff;
font-size: 14px;
margin-top: 10px;
}
.yb-player-work-item-active {
border: 1px solid var(--color-error);
background-color: var(--color-error);
}
.yb-player-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1;
}
.yb-player-center svg {
width: 50px;
height: 50px;
fill: #fff;
}
.yb-player-hide {
display: none!important;
}
.yb-player-check {
width: 20px;
height: 20px;
border-radius: 20px;
border: 1px solid #fff;
padding: 1px;
box-sizing: border-box;
}
.yb-player-check-active {
background-color: var(--color-error);
position: relative;
}
.yb-player-check-active::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 40%;
height: 40%;
border-radius: 50%;
background-color: #fff;
}
.yb-player-ellipsis {
white-space: nowrap; /* 防止文本换行 */
overflow: hidden; /* 隐藏溢出的内容 */
text-overflow: ellipsis; /* 显示省略符号来代表被修剪的文本 */
}
.yb-player-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 2;
}
.yb-player-toolbar {
position: absolute;
background-color: #000;
color: #fff;
font-size: 14px;
transform-origin: top right;
animation: toolbar .05s linear both;
z-index: 2;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
overflow-y: auto;
}
.yb-player-toolbar-item {
padding: 10px 20px;
border-bottom: 1px solid #333;
display: flex;
align-items: center;
min-width: 100px;
}
.yb-player-toolbar-item span:first-child {
flex: 1;
}
.yb-player-popup {
position: absolute;
z-index: 3;
background-color: rgba(0, 0, 0, 0.5);
box-sizing: border-box;
padding: 20px;
display: flex;
flex-direction: column;
}
.yb-player-popup-title {
font-size: 16px;
color: #fff;
text-align: center;
height: 25px;
line-height: 25px;
}
.yb-player-popup-close {
position: absolute;
top: 20px;
left: 20px;
width: 25px;
height: 25px;
border-radius: 20px;
background-color: var(--color-error);
}
.yb-player-popup-close::after, .yb-player-popup-close::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 2px;
height: 16px;
background-color: #fff;
}
.yb-player-popup-close::after {
transform: translate(-50%, -50%) rotateZ(45deg);
}
.yb-player-popup-close::before {
transform: translate(-50%, -50%) rotateZ(-45deg);
}
.yb-player-popup-content {
flex: 1;
overflow-y: auto;
width: 100%;
}
.yb-player-popup-portrait {
bottom: 0;
left: 0;
right: 0;
max-height: 80%;
animation: popupBottom .2s linear both;
}
.yb-player-popup-landscape {
width: 50%;
right: 0;
top: 0;
bottom: 0;
animation: popupRight .2s linear both;
}
.yb-player-popup-center {
min-width: 50%;
max-width: 80%;
max-height: 80%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
animation: popupCenter .2s linear both;
}
.yb-player-icon {
position: relative;
font-size: 12px;
padding: 0 5px;
border-radius: 5px;
border: 1px solid #fff;
height: 20px;
line-height: 20px;
}
.yb-player-icon-close::before {
content: '';
position: absolute;
width: 105%;
height: 1px;
background-color: #fff;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) rotateZ(30deg);
}
.yb-player-icon svg {
width: 17px;
height: 17px;
fill: #fff;
}
.spinner {
width: 60px;
height: 60px;
animation: rotate 1.4s linear infinite;
}
.spinner circle {
stroke: #ffffff;
stroke-width: 4;
stroke-dasharray: 80, 200;
stroke-dashoffset: 0;
animation: dash 1.4s ease-in-out infinite;
stroke-linecap: round;
}
/* 安全区域 */
.yb-player-controls-safearea {
padding-right: max(var(--safe-area-inset-left), var(--safe-area-inset-right));
padding-left: max(var(--safe-area-inset-left), var(--safe-area-inset-right));
padding-bottom: var(--safe-area-inset-bottom);
}
.yb-player-header-safearea {
padding-right: max(var(--safe-area-inset-left), var(--safe-area-inset-right));
padding-left: max(var(--safe-area-inset-left), var(--safe-area-inset-right));
padding-top: var(--safe-area-inset-top);
}
.yb-player-locks-safearea .yb-player-lock-left {
left: max(var(--safe-area-inset-left), var(--safe-area-inset-right));
}
.yb-player-locks-safearea .yb-player-lock-right{
right: max(var(--safe-area-inset-left), var(--safe-area-inset-right));
}
/* 横向滚动样式 */
.horizontal-scroll {
white-space: nowrap;
overflow: hidden;
}
.horizontal-content {
display: inline-block;
padding-right: 50px;
animation: horizontal-scroll 15s linear infinite;
}
.horizontal-scroll:hover .horizontal-content {
animation-play-state: paused;
}
/* 动画定义 */
@keyframes horizontal-scroll {
0% {
transform: translateX(0%);
}
100% {
transform: translateX(-100%);
}
}
/* 动画效果 */
@keyframes rotate {
100% {
transform: rotate(360deg);
}
}
@keyframes dash {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -35;
}
100% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -124;
}
}
@keyframes show-toast {
0% {
transform: translate(-50%, 200px);
}
100% {
transform: translate(-50%, 0);
}
}
@keyframes hide-toast {
0% {
transform: translate(-50%, 0);
}
100% {
transform: translate(-50%, 200px);
}
}
@keyframes capture {
0% {
transform: translate(-50%, -50%) scale(1.3);
}
100% {
transform: translate(-50%, -50%) scale(1);
}
}
@keyframes toolbar {
0% {
transform: scale(0.2);
}
100% {
transform: scale(1);
}
}
@keyframes popupBottom {
0% {
transform: translateY(100%);
}
100% {
transform: translateY(0);
}
}
@keyframes popupRight {
0% {
transform: translateX(100%);
}
100% {
transform: translateX(0);
}
}
@keyframes popupCenter {
0% {
transform: translate(-50%, -50%) scale(0);
}
100% {
transform: translate(-50%, -50%) scale(1);
}
}

View File

@@ -0,0 +1,671 @@
"use strict";
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; }
function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); }
function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } }
function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
var YbDanmu = /*#__PURE__*/function () {
function YbDanmu(container, data) {
var config = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
_classCallCheck(this, YbDanmu);
this.container = typeof container == 'string' ? document.querySelector(container) : container;
this.config = _objectSpread(_objectSpread({}, YbDanmu.DEFAULT_CONFIG), config);
this.data = data;
this.filter = new YbDanmuFilter(this.config.filter); //过滤规则
this.list = this._filterData(data);
this.runline = []; //正在跑动的弹幕列表
this.canvas = null; //canvas元素
this.ctx = null; //canvas实例
this.paused = true; //是否暂停
this.currentTime = 0; //当前时间轴
this._animation = null; //动画实例
}
return _createClass(YbDanmu, [{
key: "load",
value:
//加载弹幕
function load() {
if (this.container) {
var canvas = document.createElement('CANVAS');
this.container.appendChild(canvas);
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.updateSize();
}
}
//卸载弹幕
}, {
key: "unload",
value: function unload() {
this._clearAnimationFrame();
this.list = [];
this.runline = [];
if (this.ctx) this.ctx = null;
if (this.canvas) {
this.canvas.remove();
this.canvas = null;
}
}
//更新数据
}, {
key: "setData",
value: function setData(data) {
this.data = data;
this.reset();
}
//动态配置
}, {
key: "setConfig",
value: function setConfig(key, value) {
this.config[key] = value;
}
//播放弹幕
}, {
key: "play",
value: function play() {
this.paused = false;
this._render();
}
//暂停弹幕
}, {
key: "pause",
value: function pause() {
this._clearAnimationFrame();
this.paused = true;
}
//跳转弹幕
}, {
key: "seek",
value: function seek(time) {
this.currentTime = time;
this.reset();
}
//更新时间轴(需要外部更新时间轴否则弹幕不会运行)
}, {
key: "time",
value: function time(_time) {
var _this = this;
//计算本次更新时间和上次时间的时间差
var diffrenece = _time - this.currentTime;
//记录新时间
this.currentTime = _time;
//新列表
var newList = [];
//跑动列表
var runline = [];
//循环弹幕列表
this.list.forEach(function (danmu) {
//当前时间减去弹幕显示时间
var range = _this.getCurrentTime() - danmu.time;
//获取当前时间和上次时间的时间差以内的弹幕
if (range >= 0 && range < Math.abs(diffrenece)) runline.push(danmu);
//记录不在范围内的弹幕
else newList.push(danmu);
});
//去掉已经渲染的弹幕
this.list = newList;
//计算需要渲染弹幕的布局
for (var i = 0; i < runline.length; i++) {
var bar = this._layout(runline[i]);
if (bar) this.runline.push(bar);
}
//排序弹幕
this.runline.sort(function (a, b) {
return a.time - b.time;
});
}
//清空画布
}, {
key: "clear",
value: function clear() {
this.ctx && this.ctx.clearRect(0, 0, this.canvas.offsetWidth, this.canvas.offsetHeight);
}
//显示弹幕舞台
}, {
key: "show",
value: function show() {
this.container.style.visibility = 'visible';
}
//隐藏弹幕舞台
}, {
key: "hide",
value: function hide() {
this.container.style.visibility = 'hidden';
}
//更新尺寸
}, {
key: "updateSize",
value: function updateSize() {
var width = this.container.offsetWidth;
var height = this.container.offsetHeight;
var dpr = this.config.accuracy == 'auto' ? window.devicePixelRatio : this.config.accuracy;
this.canvas.width = width * dpr;
this.canvas.height = height * dpr;
this.canvas.style.width = width;
this.canvas.style.height = height;
this.ctx.scale(dpr, dpr);
}
//重置数据和画布
}, {
key: "reset",
value: function reset() {
var _this2 = this;
this.clear();
this.list = this._filterData(this.data);
this.runline = [];
var duration = this.canvas.offsetWidth / (this.config.speed * this.config.playbackRate) / 60; //滚动周期除以60是因为为大概每秒60帧根据滚动周期去获取可能出现在屏幕上的弹幕
var newList = [];
var runline = [];
this.list.forEach(function (danmu) {
var range = _this2.getCurrentTime() - danmu.time;
//重置数据的时候,考虑到播放进度可能在中间,未避免弹幕丢失,将时间误差控制在滚动周期以内
if (range >= 0 && range < duration) runline.push(danmu);
//记录不在范围内的弹幕
else newList.push(danmu);
});
//去掉已经渲染的弹幕
this.list = newList;
//计算渲染弹幕的布局
for (var i = 0; i < runline.length; i++) {
var bar = this._layout(runline[i]);
if (bar) this.runline.push(bar);
}
this.runline.sort(function (a, b) {
return a.time - b.time;
}); //排序弹幕
}
}, {
key: "refresh",
value: function refresh() {
this.updateSize();
this.reset();
}
//弹幕是否正在显示
}, {
key: "getVisible",
value: function getVisible() {
var _this$container$style;
return this.container ? ((_this$container$style = this.container.style) === null || _this$container$style === void 0 ? void 0 : _this$container$style.visibility) == 'visible' : false;
}
//获取时间进度
}, {
key: "getCurrentTime",
value: function getCurrentTime() {
return this.currentTime + this.config.timeDiffrence; //加上时间差
}
/**
* 发送弹幕
* @param {Object} danmu 弹幕数据
* @param {Boolean}border 使用边框
*/
}, {
key: "send",
value: function send(danmu, border) {
this.data.push(danmu);
this.runline.push(this._layout(_objectSpread(_objectSpread({}, danmu), {}, {
time: danmu.time + this.config.timeDiffrence
}), border, true)); //强制显示弹幕
}
//插入弹幕数据,但不会渲染
}, {
key: "insert",
value: function insert(danmu) {
this.data.push(danmu);
this.list.push(danmu);
}
//渲染弹幕
}, {
key: "_render",
value: function _render() {
var _this3 = this;
this._clearAnimationFrame();
if (this.paused) return;
if (this.runline.length) {
this.clear();
for (var i = 0; i < this.runline.length; i++) {
var b = this.runline[i];
//移除渲染完毕的弹幕
if (b.left + b.width <= 0 || b.boxWidth <= 0) {
this.runline.splice(i, 1);
i--;
continue;
}
// b.speed = this._detectionBump(b);//碰撞检测,保留位置,可能不需要
var speed = (this.config.speed + b.speed) * this.config.playbackRate; //弹幕随机的速度加上默认设置速度再乘以倍速,才是真正移动的速度
if (b.mode == 1 || !b.mode) b.left -= speed; //滚动弹幕将x轴位置减去速度实现滚动动画
else b.boxWidth -= speed; //顶端和底端弹幕,将记录的画布宽度减去速度,实现渲染时长
//非滚动弹幕当boxWidth大于画布宽度时则不绘制避免弹幕提前出现
if (b.mode == 1 || !b.mode || b.boxWidth <= this.canvas.offsetWidth) this._drawText(b);
}
}
this._animation = window.requestAnimationFrame(function () {
return _this3._render();
});
}
/**
* 计算弹幕布局
* @param { Object|String } danmu 弹幕对象
* @param {Boolean} border 是否使用边框
* @param {Boolean} force 是否强制显示
*/
}, {
key: "_layout",
value: function _layout(danmu, border, force) {
var ctx = this.ctx;
var canvas = this.canvas;
var config = this.config;
var boxWidth = canvas.offsetWidth;
var fontSize = danmu.fontSize || config.fontSize;
var fontFamily = danmu.fontFamily || config.fontFamily;
var fontScale = config.fontScale;
var color = danmu.color || config.color;
var mode = danmu.mode || 1;
var text = danmu.text;
var time = danmu.time;
ctx.font = "".concat(fontSize * fontScale, "px ").concat(fontFamily); //设置字体大小和样式
var width = Math.ceil(ctx.measureText(text).width); //测算文本宽度
var speed = mode == 1 ? Math.random() * 0.1 : 0; //随机移动速度(弹幕一帧移动的距离,加上随机数,可以让弹幕滚动更加有层次感)
var offset = (this.getCurrentTime() - time) * 60 * (config.speed + speed); //根据弹幕出现时间和当前时间轴的时间差,计算弹幕的初始偏移值
var left = mode == 1 ? boxWidth - offset : boxWidth / 2 - width / 2; //获取弹幕x轴位置
var track = this._getTrack(fontSize, mode, offset, force); //获取弹幕y轴位置
var top = track.top;
if (top > -1) {
return {
mode: mode,
text: text,
time: time,
fontSize: fontSize,
color: color,
top: top,
left: left,
speed: track.isOverlap ? speed + 2 + Math.random() * 0.5 : speed,
//如果是重叠的弹幕则加快速度,避免一直重叠,这样观感不好
width: width,
boxWidth: boxWidth - offset,
//顶部和底部弹幕的显示时间根据舞台宽度和移动速度决定
border: border //自己发送的弹幕需要加上边框,作区别显示
};
}
return false;
}
/**
* 绘制文字
* @param {Object} danmu 弹幕对象
* @param {String} mode 弹幕模式 1-滚动 4-底部 5-顶部
*/
}, {
key: "_drawText",
value: function _drawText(danmu) {
var mode = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
var ctx = this.ctx;
var config = this.config;
if (ctx) {
ctx.beginPath();
var fontSize = danmu.fontSize || config.fontSize;
var fontFamily = danmu.fontFamily || config.fontFamily;
var color = danmu.color || config.color;
var opacity = config.opacity;
var fontScale = config.fontScale;
var newFontSize = fontSize * fontScale;
ctx.font = "".concat(fontSize * fontScale, "px ").concat(fontFamily);
ctx.strokeStyle = YbDanmu.colorToRgba(YbDanmu.getStrokeColor(color), opacity);
ctx.strokeText(danmu.text, danmu.left, danmu.top);
ctx.fillStyle = YbDanmu.colorToRgba(color, opacity);
ctx.fillText(danmu.text, danmu.left, danmu.top);
if (danmu.border) {
//如果带边框
ctx.strokeStyle = color; //边框颜色
ctx.lineWidth = 2; //边框宽度
ctx.strokeRect(danmu.left, danmu.top - newFontSize, danmu.width, newFontSize + config.lineGap); //绘制边框
}
ctx.closePath();
}
}
/**
* 计算弹幕应该放在哪条轨道
* @param fontSize弹幕字体大小
* @param mode弹幕模式1-滚动 4-底部 5-顶部
* @param offset弹幕初始x轴位置偏差值
* @param force是否强制显示用于弹幕发送
*/
}, {
key: "_getTrack",
value: function _getTrack(fontSize) {
var mode = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
var offset = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
var force = arguments.length > 3 ? arguments[3] : undefined;
//canvas绘制文字x,y坐标是按文字左下角计算预留30px
var canvas = this.canvas;
var isOverlap = false; //是否重叠
var nowTop = -1; //最终返回的顶部距离
var minLen = -1; //最小弹幕数量
var minTop = -1; //最小弹幕数量轨道
var boxWidth = canvas.offsetWidth;
var boxHeight = canvas.offsetHeight;
var config = this.config;
var lineGap = config.lineGap;
var top = config.top;
var bottom = config.bottom;
var fontScale = config.fontScale;
var newFontSize = fontSize * fontScale;
var reserve = 5; //画布预留位置(给画布底部流出一段距离)不预留可能导致最后一个轨道的弹幕显示不全
var trackLen = Math.floor((boxHeight - top - bottom) / (newFontSize + lineGap)); //计算轨道数量
var tracks = [];
if (mode == 4) {
//弹幕模式为底端,从底部开始加载,所以要用画布高度减去轨道高度,才是底端弹幕渲染的轨道高度
for (var i = 0; i < trackLen; i++) tracks.push(boxHeight - (i * newFontSize + i * lineGap + top + reserve)); //排列轨道
} else {
for (var _i = 0; _i < trackLen; _i++) tracks.push((_i + 1) * newFontSize + _i * lineGap + top); //排列轨道
}
for (var _i2 = 0; _i2 < tracks.length; _i2++) {
var trackTop = tracks[_i2];
//当前轨道上有多少条弹幕
var danmus = this.runline.filter(function (danmu) {
return danmu.top < trackTop + newFontSize && danmu.fontSize * fontScale + danmu.top > trackTop && danmu.mode == mode;
});
if (danmus.length < minLen || minLen == -1) {
//获取最少弹幕数量的轨道
minLen = danmus.length;
minTop = trackTop;
}
if (danmus.length == 0) {
//当前轨道没有弹幕运行,直接插入该轨道
nowTop = trackTop;
break;
} else {
if (danmus.length > 0 && mode == 1) {
//当前轨道有弹幕运行,且弹幕模式是滚动模式时
var arr = danmus.map(function (danmu) {
return danmu.left + danmu.width;
});
var max = Math.max.apply(Math, _toConsumableArray(arr)); //获取当前轨道最右的弹幕
//如果当前轨道还有空位则将弹幕放入当前轨道
if (max < boxWidth - offset - 10) {
//预留10像素间隔
nowTop = trackTop;
break;
}
}
}
}
if (nowTop == -1 && (!config.overlap || force)) {
//没有找到可插入的轨道,但没有开启防重叠或者需要强制显示弹幕,这个弹幕弹幕会无视原来的弹幕开启新的弹幕墙
nowTop = minTop; //将弹幕强制插入最少弹幕数量的轨道
isOverlap = true; //重叠弹幕
}
return {
top: nowTop + top,
//加上上边距top
isOverlap: isOverlap
};
}
//过滤数据
}, {
key: "_filterData",
value: function _filterData(data) {
var _this4 = this;
var config = this.config;
var disableScroll = config.disableScroll;
var disableTop = config.disableTop;
var disableBottom = config.disableBottom;
return YbPlayer.deepClone(data.filter(function (item) {
return (item.mode != 1 || !disableScroll) && (item.mode != 5 || !disableTop) && (item.mode != 4 || !disableBottom) && !_this4.filter.filter(item.text);
}));
}
}, {
key: "_clearAnimationFrame",
value: function _clearAnimationFrame() {
if (this._animation) {
window.cancelAnimationFrame(this._animation);
this._animation = null;
}
}
}], [{
key: "parseBiliXml",
value:
//格式化b站XML文件弹幕
function parseBiliXml(content) {
var pattern = /<d\b[^>]*>(.*?)<\/d\s*>/g;
var danmuku = [];
var match = '';
while ((match = pattern.exec(content)) !== null) {
var attr = match[0].match(/p=\"*([\s\S]*?)\"/)[1];
var attrs = attr.split(',');
danmuku.push({
mode: attrs[1],
time: Number(attrs[0]),
color: YbDanmu.decimalToRgb(Number(attrs[3])),
fontSize: attrs[2],
text: match[1]
});
}
return danmuku;
}
//格式化b站ssa文件弹幕
}, {
key: "parseBiliSsa",
value: function parseBiliSsa(content) {
var lines = content.split('\n');
var events = [];
var currentSection;
lines.forEach(function (line) {
line = line.trim();
if (line.startsWith('[') && line.endsWith(']')) {
currentSection = line.slice(1, -1).toLowerCase();
return;
}
if (currentSection === 'events') {
if (line.toLowerCase().startsWith('format:')) {
// 解析事件格式行
var format = line.slice(7).split(',').map(function (item) {
return item.trim().toLowerCase();
});
// 存储事件格式定义
} else if (line.toLowerCase().startsWith('dialogue:')) {
// 解析对话行
var dialogueValues = line.slice(10).split(',').map(function (item) {
return item.trim();
});
// 注意:文本部分可能包含逗号,所以不能简单分割
var textIndex = 9; // 根据format文本通常是第9个字段从0开始计数
var text = dialogueValues.slice(9).join(','); // 合并文本部分
var dialogueData = {
start: dialogueValues[1],
end: dialogueValues[2],
style: dialogueValues[3],
text: text
};
events.push(dialogueData);
}
}
});
return events.filter(function (e) {
return !e.text.includes('\\a5\\pos');
}).map(function (e) {
var style = e.text.match(/\{*([\s\S]*?)\}/)[1];
var arr = style.split('\\');
return {
mode: 1,
time: timeToSeconds(e.start),
color: arr[2].replace('c&H', '#'),
fontSize: arr[3].replace('fs', ''),
text: e.text.replace(/\{.*?\}/g, '')
};
});
}
//根据文字颜色自动生成反色边框
}, {
key: "getStrokeColor",
value: function getStrokeColor(color) {
var r, g, b;
if (color.startsWith('#')) {
color = color.length == 7 ? color : '#' + color.slice(1, 4) + color.slice(1, 4);
r = parseInt(color.slice(1, 3), 16);
g = parseInt(color.slice(3, 5), 16);
b = parseInt(color.slice(5, 7), 16);
}
if (color.startsWith('rgb')) {
var match = color.match(/rgb\(*([\s\S]*?)\)/);
var colors = match[1].split(',');
r = colors[0];
g = colors[1];
b = colors[2];
}
var $grayLevel = r * 0.299 + g * 0.587 + b * 0.144 / 255;
//判断是否是深色
if ($grayLevel < 0.5) return '#ffffff';else return '#000000';
}
}, {
key: "colorToRgba",
value: function colorToRgba(color, opacity) {
if (color.startsWith('#')) {
color = color.length == 7 ? color : '#' + color.slice(1, 4) + color.slice(1, 4);
var _str = "rgba(";
var _r = parseInt(color.slice(1, 3), 16).toString();
var _g = parseInt(color.slice(3, 5), 16).toString();
var _b = parseInt(color.slice(5, 7), 16).toString();
_str += _r + "," + _g + "," + _b + "," + opacity + ")";
return _str;
}
if (color.startsWith('rgb')) {
var str = "rgba(";
var match = color.match(/rgb\(*([\s\S]*?)\)/);
var colors = match[1].split(',');
var r = colors[0];
var g = colors[1];
var b = colors[2];
str += r + "," + g + "," + b + "," + opacity + ")";
return str;
}
}
//16进制转rgb
}, {
key: "decimalToRgb",
value: function decimalToRgb(decimal) {
var r = decimal >> 16 & 255;
var g = decimal >> 8 & 255;
var b = decimal & 255;
return 'rgb(' + r + ',' + g + ',' + b + ')';
}
}]);
}(); //弹幕过滤
_defineProperty(YbDanmu, "DEFAULT_CONFIG", {
accuracy: 1,
//绘制精度 auto-自动使用屏幕DPI 任意整数-自定义精度 高精度会导致性能问题,谨慎使用
speed: 1,
//弹幕运行速度
playbackRate: 1,
//弹幕倍速
color: '#FFFFFF',
//弹幕默认颜色
fontSize: 18,
//弹幕默认大小
fontScale: 1,
//弹幕规格(多少倍大小)
fontFamily: 'Microsoft Yahei',
//弹幕默认字体
opacity: 1,
//弹幕透明度
top: 0,
//舞台顶部间距
bottom: 0,
//舞台底部间距
lineGap: 5,
//弹幕行间距
overlap: false,
//开启防重叠
timeDiffrence: 0,
//时间差(用于校准弹幕和视频进度的时间差,一般不需要)
disableScroll: false,
//关闭滚动弹幕
disableTop: false,
//关闭顶端弹幕
disableBottom: false,
//关闭底端弹幕
disableFilter: false,
//关闭弹幕过滤
filter: [] //过滤列表
});
var YbDanmuFilter = /*#__PURE__*/function () {
function YbDanmuFilter(list) {
_classCallCheck(this, YbDanmuFilter);
this.rules = list || [];
}
// 添加
return _createClass(YbDanmuFilter, [{
key: "add",
value: function add(rule) {
//判断是否存在同类型的相同规则
var index = this.rules.findIndex(function (r) {
return r.pattern == rule.pattern && r.type == rule.type;
});
if (index > -1) return false; //如果已经存在返回false表示插入失败
this.rules.push(rule);
return true; //添加成功返回true
}
// 删除
}, {
key: "remove",
value: function remove(index) {
this.rules.splice(index, 1);
}
// 过滤弹幕
}, {
key: "filter",
value: function filter(danmu) {
var _iterator = _createForOfIteratorHelper(this.rules),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var rule = _step.value;
if (rule.type === 'string') {
// 字符串匹配
if (danmu.includes(rule.pattern)) {
return true;
}
} else if (rule.type === 'regex') {
// 正则表达式匹配
try {
var regex = new RegExp(rule.pattern);
if (regex.test(danmu)) {
return true;
}
} catch (e) {
console.error('无效的正则表达式:', rule.pattern, e);
}
}
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
return false;
}
// 获取所有规则
}, {
key: "getRules",
value: function getRules() {
return this.rules;
}
}]);
}(); //兼容new Function为了挂载到window对象上
if (typeof window != 'undefined') {
window.YbDanmu = YbDanmu;
}

View File

@@ -0,0 +1,454 @@
"use strict";
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); }
function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } }
function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
/*手势事件处理*/
var YbGesture = /*#__PURE__*/function () {
function YbGesture(player, config) {
_classCallCheck(this, YbGesture);
this.player = player;
this.config = config;
this.disabled = false; //禁用手势事件
this._touchstartX = null;
this._touchstartY = null;
this._touchmoveX = null;
this._touchmoveY = null;
this._clickTime = null;
this._touchTime = null;
this._threshold = 20;
this._singleTouchTime = 150; //单击间隔时间
this._longTouchTime = 500; //长按间隔时间
this._mousedown = null;
this._isMove = null; //是否滑动
this._isVolume = null; //是否音量调节
this._isLight = null; //是否亮度调节
this._isProgress = null; //是否进度调节
this._seekTime = null; //跳转时间
this._seekVolume = null; //改变音量
this._seekLight = null; //改变亮度
this._touchTimer = null; //触摸定时器
this._longTimer = null; //长按定时器
this._centerTimer = null; //中间元素隐藏定时器
}
return _createClass(YbGesture, [{
key: "resetTouch",
value: function resetTouch() {
this._touchstartX = null;
this._touchstartY = null;
this._touchmoveX = null;
this._touchmoveY = null;
this._touchTime = null;
this._clickTime = null;
this._isMove = null;
this._isVolume = null;
this._isLight = null;
this._isProgress = null;
this._seekTime = null;
this._seekVolume = null;
this._seekLight = null;
}
}, {
key: "touchstart",
value: function touchstart(e) {
var _this = this;
if (this.stopPropagation(e)) return;
this._clearTouchTimer();
this._clearLongTimer();
var touch = e.touches[0];
this._touchstartX = touch.pageX;
this._touchstartY = touch.pageY;
this._clickTime++;
if (this._clickTime == 1) {
//第一次点击
this._touchTimer = window.setTimeout(function () {
_this._touchTime = _this._singleTouchTime;
}, this._singleTouchTime);
this._longTimer = window.setTimeout(function () {
if (_this._touchmoveX <= _this._threshold && _this._touchmoveY <= _this._threshold && !_this.config.disableLongPress) {
//当滑动距离不超过阙值时,且为禁止长按,才会执行长按
_this._touchTime = _this._longTouchTime;
_this.longPress(); //长按事件
}
}, this._longTouchTime);
}
}
}, {
key: "touchmove",
value: function touchmove(e) {
if (this.stopPropagation(e)) return;
if (this._clickTime > 0) {
var touch = e.touches[0];
this._touchmoveX = Math.abs(touch.pageX - this._touchstartX);
this._touchmoveY = Math.abs(touch.pageY - this._touchstartY);
//判断用户是否进行滑动,只要判断一次滑动,当前触摸事件就一直是滑动
if (!this._isMove) this._isMove = this._touchmoveY > this._threshold || this._touchmoveX > this._threshold;
//第二次点击 未开启全屏 未开启非全拼手势操作 锁屏中 滑动距离不大于阙值 触摸时间超过长按时间
if (this._clickTime == 2 || !this._isMove || this._touchTime == this._longTouchTime || !this.player.getFullscreen() && this.config.disableUnFullscreenEvent || this.player.disabled || this.disabled) return;
//根据视频方向,反转触摸位置和滑动距离
var container = this.player.container;
var containerWidth = container.offsetWidth;
var containerHeight = container.offsetHeight;
var touchX = this._touchstartX;
var touchY = this._touchstartY;
var offsetX = this._touchmoveX;
var offsetY = this._touchmoveY;
//点击在左边并且y轴滑动距离比x轴滑动距离大的时候执行调节亮度操作
if (touchX < containerWidth / 2 && offsetY > offsetX && !this._isVolume && !this._isProgress && !this.config.disableLight) {
this._isLight = true;
var initial = this.config.initialLight;
var max = 1;
var deltaY = touch.pageY - this._touchstartY;
// 计算滑动距离与容器宽度的比例
var percentDelta = offsetY / containerHeight;
// 根据比例和滑动可控制最大时长计算时间变化量
var delta = percentDelta * max;
// 计算新的时间位置
this._seekLight = deltaY < 0 ? initial + delta : initial - delta;
this._seekLight = this._seekLight > max ? max : this._seekLight < 0 ? 0 : this._seekLight;
this.setLight(this._seekLight);
}
//点击在右边并且y轴滑动距离比x轴滑动距离大的时候执行调节音量操作
if (touchX > containerWidth / 2 && offsetY > offsetX && !this._isLight && !this._isProgress && !this.config.disableVolume) {
this._isVolume = true;
var initial = this.config.initialVolume;
var max = 1;
var deltaY = touch.pageY - this._touchstartY;
// 计算滑动距离与容器宽度的比例
var percentDelta = offsetY / containerHeight;
// 根据比例和滑动可控制最大时长计算时间变化量
var delta = percentDelta * max;
// 计算新的时间位置
this._seekVolume = deltaY < 0 ? initial + delta : initial - delta;
this._seekVolume = this._seekVolume > max ? max : this._seekVolume < 0 ? 0 : this._seekVolume;
this.setVolume(this._seekVolume);
}
//y轴滑动距离比x轴滑动距离小的时候执行进度调节 不能为直播源 视频必须加载到数据
var video = this.player.video;
var duration = this.player.getDuration();
if (offsetY < offsetX && !this._isLight && !this._isVolume && !this.player.isLive && duration && !this.config.disableProgress) {
this._isProgress = true;
var deltaX = touch.pageX - this._touchstartX;
// 计算滑动距离与容器宽度的比例
var percentDelta = offsetX / containerWidth;
// 根据比例和滑动可控制最大时长计算时间变化量
var timeDelta = percentDelta * Math.min(180, duration);
// 计算新的时间位置
this._seekTime = deltaX > 0 ? video.currentTime + timeDelta : video.currentTime - timeDelta;
this._seekTime = this._seekTime > duration ? duration : this._seekTime < 0 ? 0 : this._seekTime;
this.setProgress(this._seekTime);
}
}
}
}, {
key: "touchend",
value: function touchend(e) {
var _this2 = this;
if (this.stopPropagation(e)) return;
this._clearTouchTimer();
this._clearLongTimer();
//当触摸事件等于
if (this._touchTime == this._longTouchTime) this.stopLongPress(); //停止长按
//点击次数大于零并且触摸时间小于单击时间并且滑动距离不超过阙值
else if (this._clickTime > 0 && this._touchTime < this._singleTouchTime && this._touchmoveX <= this._threshold && this._touchmoveY <= this._threshold) {
if (this._clickTime == 1 && !this.config.disableSingleClick) {
//第一次点击
this._touchTimer = window.setTimeout(function () {
_this2.singleClick();
}, this._singleTouchTime);
} else if (this._clickTime == 2 && !this.config.disableDoubleClick) this.doubleClick(); //第二次点击
else this.resetTouch();
} else {
var video = this.player.video;
if (this._isProgress && this._seekTime >= 0 && video) video.currentTime = this._seekTime;
if (this._isLight && this._seekLight >= 0) {
this.config.initialLight = this._seekLight; //更新初始化亮度,注意这并不代表系统亮度被改变
var data = {
seekLight: this._seekLight
};
this.player.emit('seeklight', this._seekLight);
}
if (this._isVolume && this._seekVolume >= 0) {
//更新初始化音量,注意这并不代表系统音量被改变
this.config.initialVolume = this._seekVolume;
var data = {
seekVolume: this._seekVolume
};
this.player.emit('seekvolume', this._seekVolume);
}
this.resetTouch();
}
}
}, {
key: "singleClick",
value: function singleClick() {
//单击事件
this.resetTouch();
if (this.player.controls) {
if (this.player.getControls()) this.player.hideControls();else this.player.showControls();
}
this.player.emit('singleclick');
}
}, {
key: "doubleClick",
value: function doubleClick() {
//双击事件
this.resetTouch();
this.player.toggle();
this.player.emit('doubleclick');
}
}, {
key: "longPress",
value: function longPress() {
//长按事件
this.setPlaybackRate(2.0);
this.player.emit('longpress');
}
}, {
key: "stopLongPress",
value: function stopLongPress() {
//停止长按事件
this.resetTouch();
this.setPlaybackRate(1.0);
this.player.emit('stoplongpress');
}
//注册手势事件
}, {
key: "load",
value: function load() {
var _this3 = this;
var container = this.player.container;
var wrapperEl = container.getElementsByClassName('yb-player-wrapper')[0];
//绑定触摸监听
wrapperEl.ontouchstart = function (e) {
_this3.touchstart(e);
};
wrapperEl.ontouchmove = function (e) {
_this3.touchmove(e);
};
wrapperEl.ontouchend = function (e) {
_this3.touchend(e);
};
wrapperEl.ontouchcancel = function (e) {
_this3.touchend(e);
};
//电脑端模拟触摸
wrapperEl.onmousedown = function (e) {
if ('ontouchstart' in window || window.DocumentTouch && document instanceof DocumentTouch) return; //设备支持触屏则不触发mousedown
_this3._mousedown = true;
_this3.touchstart({
target: e.target,
touches: [{
pageX: e.pageX,
pageY: e.pageY
}]
});
};
wrapperEl.onmousemove = function (e) {
if (!_this3._mousedown) return;
_this3.touchmove({
target: e.target,
touches: [{
pageX: e.pageX,
pageY: e.pageY
}]
});
};
wrapperEl.onmouseup = function (e) {
if (!_this3._mousedown) return;
_this3._mousedown = false;
_this3.touchend(e);
};
}
//注销手势事件
}, {
key: "unload",
value: function unload() {
this._clearTouchTimer();
this._clearLongTimer();
this._clearCenterTimer();
var container = this.player.container;
var wrapperEl = container.getElementsByClassName('yb-player-wrapper')[0];
if (wrapperEl) {
wrapperEl.ontouchstart = null;
wrapperEl.ontouchmove = null;
wrapperEl.ontouchend = null;
wrapperEl.onmousedown = null;
wrapperEl.onmousemove = null;
wrapperEl.onmouseup = null;
wrapperEl = null;
}
}
}, {
key: "setConfig",
value: function setConfig(key, value) {
this.config[key] = value;
}
}, {
key: "disable",
value: function disable() {
this.disabled = true;
}
}, {
key: "enable",
value: function enable() {
this.disabled = false;
}
//设置倍速
}, {
key: "setPlaybackRate",
value: function setPlaybackRate(playbackRate) {
var video = this.player.video;
if (!video) return;
if (playbackRate == video.playbackRate) return;
video.playbackRate = playbackRate;
// if ( ![1, 1.0].includes(playbackRate) ) {
// var rateEl = document.createElement('DIV')
// rateEl.innerHTML = `
// <div class="yb-player-rate">
// <div class="yb-player-rate-icon">
// <i></i><i></i><i></i>
// </div>
// <span class="yb-player-rate-span">${playbackRate + '倍速播放中'}</span>
// </div>
// `
// var wrapperEl = this.player.container.getElementsByClassName('yb-player-wrapper')[0]
// if ( wrapperEl ) wrapperEl.appendChild(rateEl)
// } else {
// var rateEl = this.player.container.getElementsByClassName('yb-player-rate')[0]
// if ( rateEl )rateEl.remove()
// }
}
//设置音量
}, {
key: "setVolume",
value: function setVolume(volume) {
var _this4 = this;
this._clearCenterTimer();
var container = this.player.container;
var wrapperEl = container.getElementsByClassName('yb-player-wrapper')[0];
var div = container.getElementsByClassName('yb-player-center-value')[0];
if (!div) {
div = document.createElement('DIV');
div.setAttribute('class', 'yb-player-center yb-player-center-value');
wrapperEl.appendChild(div);
}
div.innerHTML = "\n\t\t\t<span>\u97F3\u91CF</span>\n\t\t\t<span>".concat((volume / 1 * 100).toFixed(0), "%</span>\n\t\t");
this._centerTimer = window.setTimeout(function () {
_this4.hideCenterValue();
}, 1000);
}
//设置亮度
}, {
key: "setLight",
value: function setLight(light) {
var _this5 = this;
this._clearCenterTimer();
var container = this.player.container;
var wrapperEl = container.getElementsByClassName('yb-player-wrapper')[0];
var div = container.getElementsByClassName('yb-player-center-value')[0];
if (!div) {
div = document.createElement('DIV');
div.setAttribute('class', 'yb-player-center yb-player-center-value');
wrapperEl.appendChild(div);
}
div.innerHTML = "\n\t\t\t<span>\u4EAE\u5EA6</span>\n\t\t\t<span>".concat((light / 1 * 100).toFixed(0), "%</span>\n\t\t");
this._centerTimer = window.setTimeout(function () {
_this5.hideCenterValue();
}, 1000);
}
//设置进度
}, {
key: "setProgress",
value: function setProgress(time) {
var _this6 = this;
this._clearCenterTimer();
var container = this.player.container;
var video = this.player.video;
var wrapperEl = container.getElementsByClassName('yb-player-wrapper')[0];
var div = container.getElementsByClassName('yb-player-center-value')[0];
if (!div) {
div = document.createElement('DIV');
div.setAttribute('class', 'yb-player-center yb-player-center-value');
wrapperEl.appendChild(div);
}
div.innerHTML = "\n\t\t\t".concat(YbPlayer.timeFormat(time), " / ").concat(YbPlayer.timeFormat(this.player.getDuration()), "\n\t\t");
this._centerTimer = window.setTimeout(function () {
_this6.hideCenterValue();
}, 1000);
}
//删除手势事件产生的中间控件
}, {
key: "hideCenterValue",
value: function hideCenterValue() {
var div = this.player.container.getElementsByClassName('yb-player-center-value')[0];
if (div) {
div.remove();
div = null;
}
}
/**
* 阻止冒泡
* @param e 点击实例
*/
}, {
key: "stopPropagation",
value: function stopPropagation(e) {
var container = this.player.container;
var gestureEls = container.getElementsByClassName('yb-player-gesture'); //允许手势事件冒泡元素标记
var isGesture = false; //是否点击了非手势控制标记的元素
for (var i = 0; i < gestureEls.length; i++) {
isGesture = gestureEls[i].contains(e.target); //对比元素
if (isGesture) break; //如果找到了,打断循环
}
if (isGesture) return false; //允许手势事件
var danmuEl = container.getElementsByClassName('yb-player-danmu')[0];
var subtitleEl = container.getElementsByClassName('yb-player-subtitle')[0];
var panoEl = container.getElementsByClassName('yb-player-pano')[0];
var video = this.player.video;
// console.log('target', e.target);
// console.log('video', video.contains(e.target));
// console.log('danmuEl', danmuEl.contains(e.target));
// console.log('subtitleEl', subtitleEl.contains(e.target));
if (video && video.contains(e.target) || danmuEl && danmuEl.contains(e.target) || subtitleEl && subtitleEl.contains(e.target) || panoEl && panoEl.contains(e.target)) return false;
return true;
}
/***** 清除定时器 *****/
}, {
key: "_clearTouchTimer",
value: function _clearTouchTimer() {
if (this._touchTimer) {
window.clearTimeout(this._touchTimer);
this._touchTimer = null;
}
}
}, {
key: "_clearLongTimer",
value: function _clearLongTimer() {
if (this._longTimer) {
window.clearTimeout(this._longTimer);
this._longTimer = null;
}
}
}, {
key: "_clearCenterTimer",
value: function _clearCenterTimer() {
if (this._centerTimer) {
window.clearTimeout(this._centerTimer);
this._centerTimer = null;
}
}
}]);
}(); //兼容new Function为了挂载到window对象上
if (typeof window != 'undefined') {
window.YbGesture = YbGesture;
}

View File

@@ -0,0 +1,406 @@
"use strict";
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); }
function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } }
function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
var YbMpeg = /*#__PURE__*/function () {
function YbMpeg() {
_classCallCheck(this, YbMpeg);
this.canvas = null;
this._src = null; //播放链接
this.duration = null; //总时长
this.jsmpeg = null; //jsmpeg实例
this.readyState = 0; //视频状态
this._event = {}; //事件对象
this._init = null; //是否已经初始化
this._isSeeking = false; //是否正在跳转
this._seekingTime = null; //正在跳转的时间
this.init();
}
//操作播放链接
return _createClass(YbMpeg, [{
key: "src",
get: function get() {
return this._src;
},
set: function set(value) {
this._src = value;
}
//操作静音
}, {
key: "muted",
get: function get() {
var _this$jsmpeg;
return ((_this$jsmpeg = this.jsmpeg) === null || _this$jsmpeg === void 0 ? void 0 : _this$jsmpeg.volume) == 0 ? true : false;
},
set: function set(value) {
if (this.jsmpeg) {
this.jsmpeg.volume = value ? 0 : 1;
this.emit('volumechange');
}
}
}, {
key: "style",
get: function get() {
return this.canvas.style;
}
//操作进度
}, {
key: "currentTime",
get: function get() {
var _this$jsmpeg2;
return (_this$jsmpeg2 = this.jsmpeg) === null || _this$jsmpeg2 === void 0 ? void 0 : _this$jsmpeg2.currentTime;
}
//设置进度
,
set: function set(value) {
if (this.jsmpeg) {
this.jsmpeg.currentTime = value;
this._isSeeking = true;
this._seekingTime = value;
this.emit('seeking');
}
}
//获取视频宽度
}, {
key: "videoWidth",
get: function get() {
var _this$jsmpeg3;
return (_this$jsmpeg3 = this.jsmpeg) === null || _this$jsmpeg3 === void 0 || (_this$jsmpeg3 = _this$jsmpeg3.video) === null || _this$jsmpeg3 === void 0 || (_this$jsmpeg3 = _this$jsmpeg3.destination) === null || _this$jsmpeg3 === void 0 ? void 0 : _this$jsmpeg3.width;
}
//获取视频高度
}, {
key: "videoHeight",
get: function get() {
var _this$jsmpeg4;
return (_this$jsmpeg4 = this.jsmpeg) === null || _this$jsmpeg4 === void 0 || (_this$jsmpeg4 = _this$jsmpeg4.video) === null || _this$jsmpeg4 === void 0 || (_this$jsmpeg4 = _this$jsmpeg4.destination) === null || _this$jsmpeg4 === void 0 ? void 0 : _this$jsmpeg4.height;
}
//获取暂停状态
}, {
key: "paused",
get: function get() {
var _this$jsmpeg5;
return (_this$jsmpeg5 = this.jsmpeg) === null || _this$jsmpeg5 === void 0 ? void 0 : _this$jsmpeg5.paused;
}
//播放事件
}, {
key: "onplay",
get: function get() {
return this._event.onplay || null;
},
set: function set(callback) {
this._event.onplay = callback;
}
//暂停事件
}, {
key: "onpause",
get: function get() {
return this._event.onpause || null;
},
set: function set(callback) {
this._event.onpause = callback;
}
//播放结束事件
}, {
key: "onended",
get: function get() {
return this._event.onended || null;
},
set: function set(callback) {
this._event.onended = callback;
}
//数据不足等待加载事件
}, {
key: "onwaiting",
get: function get() {
return this._event.onwaiting || null;
},
set: function set(callback) {
this._event.onwaiting = callback;
}
//恢复播放事件
}, {
key: "onplaying",
get: function get() {
return this._event.onplaying || null;
},
set: function set(callback) {
this._event.onplaying = callback;
}
//开始加载事件
}, {
key: "onloadstart",
get: function get() {
return this._event.onloadstart || null;
},
set: function set(callback) {
this._event.onloadstart = callback;
}
//加载到元数据事件
}, {
key: "onloadedmetadata",
get: function get() {
return this._event.onloadedmetadata || null;
},
set: function set(callback) {
this._event.onloadedmetadata = callback;
}
//加载到第一帧事件
}, {
key: "onloadeddata",
get: function get() {
return this._event.onloadeddata || null;
},
set: function set(callback) {
this._event.onloadeddata = callback;
}
//可以播放事件
}, {
key: "oncanplay",
get: function get() {
return this._event.oncanplay || null;
},
set: function set(callback) {
this._event.oncanplay = callback;
}
//加载全部数据事件
}, {
key: "oncanplaythrough",
get: function get() {
return this._event.oncanplaythrough || null;
},
set: function set(callback) {
this._event.oncanplaythrough = callback;
}
}, {
key: "oncontextmenu",
get: function get() {
var _this$canvas;
return ((_this$canvas = this.canvas) === null || _this$canvas === void 0 ? void 0 : _this$canvas.oncontextmenu) || null;
},
set: function set(callback) {
if (this.canvas) this.canvas.oncontextmenu = callback;
}
//音量改变事件
}, {
key: "onvolumechange",
get: function get() {
return this._event.onvolumechange || null;
},
set: function set(callback) {
this._event.onvolumechange = callback;
}
//总时长改变事件
}, {
key: "ondurationchange",
get: function get() {
return this._event.ondurationchange || null;
},
set: function set(callback) {
this._event.ondurationchange = callback;
}
//事件进度更新事件
}, {
key: "ontimeupdate",
get: function get() {
return this._event.ontimeupdate || null;
},
set: function set(callback) {
this._event.ontimeupdate = callback;
}
//跳转中事件
}, {
key: "onseeking",
get: function get() {
return this._event.onseeking || null;
},
set: function set(callback) {
this._event.onseeking = callback;
}
//跳转完成事件
}, {
key: "onseeked",
get: function get() {
return this._event.onseeked || null;
},
set: function set(callback) {
this._event.onseeked = callback;
}
//中断事件
}, {
key: "onabort",
get: function get() {
return this._event.onabort || null;
},
set: function set(callback) {
this._event.onabort = callback;
}
}, {
key: "init",
value: function init() {
this.canvas = document.createElement('CANVAS');
}
//设置属性
}, {
key: "setAttribute",
value: function setAttribute(attr, value) {
this.canvas.setAttribute(attr, value);
}
//移除属性
}, {
key: "removeAttribute",
value: function removeAttribute(attr) {
//如果移除的属性是src则清空src
if (attr == 'src') {
this._src = null;
} else {
this.canvas.removeAttribute(attr);
}
}
//是否包含某个节点
}, {
key: "contains",
value: function contains(target) {
return this.canvas.contains(target);
}
//配置文件
}, {
key: "setConfig",
value: function setConfig(config) {
this.config = config;
}
}, {
key: "setDecoder",
value: function setDecoder(decoder) {
this.decoder = decoder;
}
//加载视频
}, {
key: "load",
value: function load() {
var _this = this;
this.unload();
if (!this._src) return;
this.jsmpeg = new this.decoder.Player(this._src, _objectSpread(_objectSpread({
canvas: this.canvas
}, this.config), {}, {
//当源接收到所有数据时调用的回调
onSourceCompleted: function onSourceCompleted() {
_this.readyState = 4;
_this.emit('canplaythrough');
},
//当source第一次接收到数据时调用的回调
onSourceEstablished: function onSourceEstablished() {
_this.readyState = 1;
_this.emit('canplay');
_this.emit('loadedmetadata');
},
//已连接
onSourceConnected: function onSourceConnected() {
_this.emit('loadstart');
},
//视频流解码完毕
onVideoDecode: function onVideoDecode(decoder, time) {
//还未初始化
if (!_this._init) {
//更新readyState状态
_this.readyState = 3;
_this.emit('loadeddata');
_this._init = true;
} else {
//如果刚才处于卡顿中
if (_this.readyState == 2) {
_this.readyState = 3; //更改视频状态
_this.emit('playing'); //触发playing
}
//处于跳转当中,并且当前时间和跳转时间相近,则认为跳转完成
if (_this._isSeeking && Math.abs(_this.jsmpeg.currentTime - _this._seekingTime) < 0.1) {
_this._isSeeking = false;
_this._seekingTime = null;
_this.emit('seeked');
}
_this.emit('timeupdate');
}
if (_this.jsmpeg.currentTime > _this.duration) _this.duration = _this.jsmpeg.currentTime + 1; //直播流/实时流是没有总播放时长的但为了方便外部调用duration不会报错这里默认给duration实时更新
_this.emit('durationchange');
},
//连接断开
onSourceDisconnected: function onSourceDisconnected() {
_this.readyState = 0;
_this.emit('abort');
},
//当播放开始时调用的回调函数
onPlay: function onPlay() {
_this.emit('play');
},
//当播放暂停时调用的回调函数(例如当.pause()被调用或源结束时)
onPause: function onPause() {
_this.emit('pause');
},
//当播放到达源的末尾时调用(只在loop=false调用)
onEnded: function onEnded() {
_this.emit('ended');
},
//当没有足够的数据播放时调用的回调
onStalled: function onStalled() {
_this.readyState = 2;
_this.emit('waiting');
}
}));
this.jsmpeg.volume = this.config.muted ? 0 : 1;
}
//卸载视频
}, {
key: "unload",
value: function unload() {
var _this$jsmpeg6;
(_this$jsmpeg6 = this.jsmpeg) === null || _this$jsmpeg6 === void 0 || _this$jsmpeg6.destroy();
this.jsmpeg = null;
this.duration = null;
this.jsmpeg = null;
this.readyState = 0;
this._init = null;
}
//播放视频
}, {
key: "play",
value: function play() {
var _this$jsmpeg7;
(_this$jsmpeg7 = this.jsmpeg) === null || _this$jsmpeg7 === void 0 || _this$jsmpeg7.play();
}
//暂停视频
}, {
key: "pause",
value: function pause() {
var _this$jsmpeg8;
(_this$jsmpeg8 = this.jsmpeg) === null || _this$jsmpeg8 === void 0 || _this$jsmpeg8.pause();
}
//抛出事件
}, {
key: "emit",
value: function emit(name) {
var _this$_event;
for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
this._event['on' + name] && (_this$_event = this._event)['on' + name].apply(_this$_event, args);
}
//移除视频
}, {
key: "remove",
value: function remove() {
var _this$canvas2;
(_this$canvas2 = this.canvas) === null || _this$canvas2 === void 0 || _this$canvas2.remove();
this.canvas = null;
}
}]);
}(); //兼容new Function为了挂载到window对象上
if (typeof window != 'undefined') {
window.YbMpeg = YbMpeg;
}

View File

@@ -0,0 +1,225 @@
"use strict";
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); }
function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } }
function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
var YbPano = /*#__PURE__*/function () {
function YbPano(player) {
var config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
_classCallCheck(this, YbPano);
this.player = player;
this.config = _objectSpread(_objectSpread({}, YbPano.DEFAULT_CONFIG), config);
this.scene = null;
this.camera = null;
this.renderer = null;
this.geometry = null; //几何体
this.material = null; //材质
this.sphere = null;
this.videoTexture = null; //视频纹理
this.controls = null; //摄像机控制器
this._animated = null; //动画实例
}
return _createClass(YbPano, [{
key: "init",
value: function init() {
var container = this.player.container;
var boxWidth = container.offsetWidth;
var boxHeight = container.offsetHeight;
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(75, boxWidth / boxHeight, 0.1, 1000);
this.renderer = new THREE.WebGLRenderer({
preserveDrawingBuffer: true //不设置该属性,则不能将渲染结果保存为图片
});
this.renderer.setSize(boxWidth, boxHeight);
var wrapperEl = container.getElementsByClassName('yb-player-wrapper')[0];
var div = document.createElement('DIV');
div.setAttribute('class', 'yb-player-pano');
div.setAttribute('style', 'position:absolute;inset: 0;visibility: hidden;'); //第一次加载不显示场景,因为考虑到视频需要显示封面,等待播放之后再显示
div.appendChild(this.renderer.domElement);
wrapperEl.insertBefore(div, wrapperEl.getElementsByClassName('yb-player-danmu')[0]);
this.camera.position.z = 5; //相机位置
this.loadControls();
}
//加载控制器
}, {
key: "loadControls",
value: function loadControls() {
//如果设备支持陀螺仪
if (this.config.controlsType == 'orientation' && typeof DeviceOrientationEvent !== 'undefined') {
this.controls = new THREE.DeviceOrientationControls(this.camera);
this.requestOrientationPermission();
//陀螺仪控制可以开启手势事件
this.player.enableGesture();
} else {
this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableZoom = false;
this.controls.enablePan = false;
this.controls.enableDamping = this.config.enableDamping;
if (!this.config.disableDragReverse) this.controls.rotateSpeed = -0.25;
if (this.player.three == '180') {
this.controls.minAzimuthAngle = 0;
this.controls.maxAzimuthAngle = 180;
this.controls.minPolarAngle = 0;
this.controls.maxPolarAngle = 180;
}
//禁用手势事件避免冲突
this.player.disableGesture();
//如果尝试开启陀螺仪控制,但失败了,则弹出提示
if (this.config.controlsType == 'orientation') {
this.player.showToast('该设备或当前环境可能不支持陀螺仪,已自动切换为滑动');
this.config.controlsType = 'orbit';
}
}
}
//卸载控制器
}, {
key: "unloadControls",
value: function unloadControls() {
if (this.controls) {
this.controls.disconnect && this.controls.disconnect();
this.controls.dispose();
this.controls = null;
}
}
//获取控制器类型
}, {
key: "getControlsType",
value: function getControlsType() {
return this.controls ? this.controls.deviceOrientation ? 'orientation' : 'orbit' : 'close';
}
}, {
key: "setControlsType",
value: function setControlsType(type) {
this.config.controlsType = type;
this.unloadControls();
this.loadControls();
}
//请求陀螺仪权限
}, {
key: "requestOrientationPermission",
value: function requestOrientationPermission() {
var _this = this;
if (typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function') {
DeviceOrientationEvent.requestPermission().then(function (permissionState) {
if (permissionState === 'granted') {
// 用户授予权限
_this.controls.connect(); // 现在才连接和启用控件
} else {
_this.player.showToast('必须给予陀螺仪权限才能使用陀螺仪');
}
})["catch"](function (err) {
_this.player.showToast('请求陀螺仪权限失败');
});
} else {
// 理论上这个分支应该已经被 init 函数处理了,但作为备用
this.controls.connect();
}
}
//开始渲染
}, {
key: "animate",
value: function animate() {
var _this2 = this;
this._cancelAnimationFrame();
var video = this.player.video;
if (video.readyState === video.HAVE_ENOUGH_DATA) this.videoTexture.needsUpdate = true;
if (this.controls && this.controls.enabled) this.controls.update();
//判断是否有["__v_raw"]属性如果有传入该属性值否则直接传入this.scene这里是为了兼容vue3因为vue3会将这些变量变为响应式数据导致内部一些只读变量报错
//该方法来自于vue3的toRaw方法
var scene = this.scene["__v_raw"] ? this.scene["__v_raw"] : this.scene;
this.renderer.render(scene, this.camera);
this._animated = window.requestAnimationFrame(function () {
return _this2.animate();
});
}
//更新尺寸
}, {
key: "updateSize",
value: function updateSize() {
var container = this.player.container;
var boxWidth = container.offsetWidth;
var boxHeight = container.offsetHeight;
this.camera.aspect = boxWidth / boxHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(boxWidth, boxHeight);
}
//创建视频纹理
}, {
key: "createVideoSphere",
value: function createVideoSphere() {
var video = this.player.video;
this.videoTexture = new THREE.VideoTexture(video);
this.geometry = this.player.three == '360' ? new THREE.SphereGeometry(500, 60, 40) : new THREE.SphereGeometry(500, 60, 40, 0, Math.PI, 0, Math.PI);
this.geometry.scale(-1, 1, 1);
this.material = new THREE.MeshBasicMaterial({
map: this.videoTexture
});
this.sphere = new THREE.Mesh(this.geometry, this.material);
this.scene.add(this.sphere);
}
//回收内存
}, {
key: "dispose",
value: function dispose() {
this._cancelAnimationFrame();
this.unloadControls();
if (this.videoTexture) {
this.videoTexture.dispose();
this.videoTexture = null;
}
if (this.geometry) {
this.geometry.dispose();
this.geometry = null;
}
if (this.material) {
this.material.dispose();
this.material = null;
}
if (this.sphere) {
this.sphere.remove();
this.sphere = null;
}
if (this.renderer) {
this.renderer.dispose();
this.renderer = null;
}
if (this.camera) {
this.camera.remove();
this.camera = null;
}
if (this.scene) {
this.scene.remove();
this.scene = null;
}
var container = this.player.container;
var threeEl = container.getElementsByClassName('yb-player-pano')[0];
if (threeEl) threeEl.remove();
threeEl = null;
}
//取消渲染动画
}, {
key: "_cancelAnimationFrame",
value: function _cancelAnimationFrame() {
if (this._animated) {
window.cancelAnimationFrame(this._animated);
this._animated = null;
}
}
}]);
}(); //兼容new Function为了挂载到window对象上
_defineProperty(YbPano, "DEFAULT_CONFIG", {
controlsType: 'orbit',
//控制器类型 orbit-手指或鼠标拖动 orientation-陀螺仪控制
disableDragReverse: false,
//关闭反方向拖拽旋转(反向拖拽更符合人的直觉)
enableDamping: true //添加阻尼效果(滑动更顺畅)
});
if (typeof window != 'undefined') {
window.YbPano = YbPano;
}

View File

@@ -0,0 +1,440 @@
"use strict";
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); }
function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } }
function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
/*字幕处理*/
var YbSubtitle = /*#__PURE__*/function () {
function YbSubtitle(player, list, config) {
_classCallCheck(this, YbSubtitle);
this.player = player;
this.list = list || [];
this.config = config || {};
this.activeIndex = -1; //渲染字幕位置索引
this.paused = true; //停止渲染
this._animation = null; //渲染动画
}
//默认配置
return _createClass(YbSubtitle, [{
key: "unload",
value: function unload() {
this.pause();
var container = this.player.container;
var div = container.getElementsByClassName('yb-player-subtitle-text')[0];
if (div) div.remove();
}
}, {
key: "setConfig",
value: function setConfig(key, value) {
this.config[key] = value;
}
}, {
key: "play",
value: function play() {
this.paused = false;
this._render();
}
}, {
key: "pause",
value: function pause() {
this.paused = true;
if (this._animation) {
window.cancelAnimationFrame(this._animation);
this._animation = null;
}
}
//渲染字幕
}, {
key: "_render",
value: function _render() {
var _this = this;
var container = this.player.container;
var video = this.player.video;
if (!video || !container) return;
var div = container.getElementsByClassName('yb-player-subtitle-text')[0];
var wrapperEl = container.getElementsByClassName('yb-player-wrapper')[0];
var nowIndex = this.list.findIndex(function (item) {
return YbPlayer.timeToSeconds(item.startTime) <= video.currentTime && YbPlayer.timeToSeconds(item.endTime) >= video.currentTime;
});
var config = _objectSpread(_objectSpread({}, YbSubtitle.DEFAULT_CONFIG), this.config);
if (nowIndex > -1 && this.activeIndex != nowIndex) {
this.activeIndex = nowIndex;
var nowTitle = this.list[nowIndex];
if (!div) {
div = document.createElement('DIV');
div.setAttribute('class', 'yb-player-subtitle-text');
wrapperEl.appendChild(div);
}
div.style.color = config.color;
div.style.fontSize = config.fontSize + 'px';
div.style.bottom = config.bottom;
div.style.textShadow = "0 0 5px " + config.shadowColor;
div.innerHTML = "<span>".concat(nowTitle.text, "</span>");
} else {
if (div && nowIndex == -1) div.remove();
}
if (!this.paused) this._animation = window.requestAnimationFrame(function () {
return _this._render();
});
}
}], [{
key: "init",
value: function init(player, src, config) {
return new Promise(function (resolve) {
YbSubtitle.showLoading(player);
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
YbSubtitle.showResult(player, '加载字幕文件成功');
var list = src.includes('.srt') ? YbSubtitle.parseSrt(xhr.responseText) : src.includes('.ass') ? YbSubtitle.parseAss(xhr.responseText).events.data.map(function (item) {
var text = item.Text.replace(/\{.*?\}/g, ''); // 移除{}标签
text = text.replace(/\\N/g, '<br>'); // 处理换行
return _objectSpread(_objectSpread({}, item), {}, {
text: text,
startTime: item.Start,
endTime: item.End
});
}) : YbSubtitle.parseVtt(xhr.responseText).cues;
resolve(new YbSubtitle(player, list, config));
} else {
YbSubtitle.showResult(player, '加载字幕文件失败');
resolve(null);
}
xhr.abort();
}
};
xhr.onabort = function () {
xhr = null;
};
xhr.open('GET', src);
xhr.responseType = 'text';
xhr.send();
});
}
}, {
key: "showLoading",
value: function showLoading(player) {
var container = player.container;
var wrapperEl = container ? container.getElementsByClassName('yb-player-wrapper')[0] : null;
if (wrapperEl) {
var div = document.createElement('DIV');
div.setAttribute('class', 'yb-player-subtitle-loading');
div.innerHTML = '正在加载字幕文件';
wrapperEl.appendChild(div);
}
}
}, {
key: "hideLoading",
value: function hideLoading(player) {
var container = player === null || player === void 0 ? void 0 : player.container;
var loadingEl = container ? container.getElementsByClassName('yb-player-subtitle-loading')[0] : null;
if (loadingEl) loadingEl.remove();
}
}, {
key: "showResult",
value: function showResult(player, message) {
var container = player.container;
var loadingEl = container ? container.getElementsByClassName('yb-player-subtitle-loading')[0] : null;
if (loadingEl) loadingEl.innerHTML = message;
window.setTimeout(function () {
YbSubtitle.hideLoading(player);
}, 1000);
}
//格式化SRT字幕
}, {
key: "parseSrt",
value: function parseSrt(content) {
// 按空行分割字幕块
var blocks = content.trim().split(/\n\s*\n/);
var result = [];
for (var i = 0; i < blocks.length; i++) {
var lines = blocks[i].split('\n').filter(function (line) {
return line.trim() !== '';
});
if (lines.length < 3) {
continue; // 跳过无效块
}
// 解析序号
var index = parseInt(lines[0]);
if (isNaN(index)) {
throw new Error("\u65E0\u6548\u7684\u5E8F\u53F7: ".concat(lines[0]));
}
// 解析时间码
var timecodeMatch = lines[1].match(/(\d{2}):(\d{2}):(\d{2}),(\d{3})\s*-->\s*(\d{2}):(\d{2}):(\d{2}),(\d{3})/);
if (!timecodeMatch) {
throw new Error("\u65E0\u6548\u7684\u65F6\u95F4\u7801\u683C\u5F0F: ".concat(lines[1]));
}
var startTime = "".concat(parseInt(timecodeMatch[1]), ":").concat(parseInt(timecodeMatch[2]), ":").concat(parseInt(timecodeMatch[3]), ".").concat(parseInt(timecodeMatch[4]));
var endTime = "".concat(parseInt(timecodeMatch[5]), ":").concat(parseInt(timecodeMatch[6]), ":").concat(parseInt(timecodeMatch[7]), ".").concat(parseInt(timecodeMatch[8]));
// 合并文本行
var text = lines.slice(2).join('\n');
result.push({
index: index,
startTime: startTime,
endTime: endTime,
text: text
});
}
return result;
}
}, {
key: "parseAss",
value: function parseAss(content) {
var lines = content.split('\n');
var currentSection = '';
var result = {
info: {},
styles: {
format: [],
data: []
},
events: {
format: [],
data: []
}
};
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
// 跳过空行和注释
if (!line || line.startsWith(';')) continue;
// 检测段落开始
if (line.startsWith('[') && line.endsWith(']')) {
currentSection = line.slice(1, -1).toLowerCase();
continue;
}
// 根据当前段落处理内容
switch (currentSection) {
case 'script info':
parseScriptInfo(line, result.info);
break;
case 'v4+ styles':
parseStyles(line, result.styles);
break;
case 'events':
parseEvents(line, result.events);
break;
}
}
return result;
}
}, {
key: "parseVtt",
value: function parseVtt(content) {
var lines = content.split('\n');
var cues = [];
var currentCue = null;
var note = null;
var style = null;
var region = null;
// 检查文件头
if (lines.length === 0 || !lines[0].includes('WEBVTT')) {
throw new Error('无效的VTT文件: 缺少WEBVTT文件头');
}
// 解析VTT内容
for (var i = 1; i < lines.length; i++) {
var line = lines[i].trim();
// 跳过空行
if (line === '') continue;
// 检查是否是时间轴
if (line.includes('-->')) {
// 如果已有当前cue先保存它
if (currentCue) {
cues.push(currentCue);
}
// 创建新的cue
currentCue = {
identifier: null,
start: null,
end: null,
text: '',
styles: null
};
// 解析时间轴
var arrowIndex = line.indexOf('-->');
currentCue.startTime = line.substring(0, arrowIndex).trim();
currentCue.endTime = line.substring(arrowIndex + 3, arrowIndex + 15).trim();
var settingsLine = line.substring(arrowIndex + 15).trim();
// 解析设置(如果有)
if (settingsLine) currentCue.styles = parseCueSettings(settingsLine);
}
// 检查是否是标识符(数字或文本)
else if (!currentCue && /^\d+$/.test(line)) {
// 这是标识符行
if (currentCue) {
currentCue.identifier = line;
}
}
// 检查是否是注释
else if (line.startsWith('NOTE')) {
note = line.substring(4).trim();
}
// 检查是否是样式块
else if (line.startsWith('STYLE')) {
style = line.substring(5).trim();
}
// 检查是否是区域块
else if (line.startsWith('REGION')) {
region = line.substring(6).trim();
}
// 否则是文本行
else if (currentCue) {
if (currentCue.text) {
currentCue.text += '\n' + line;
} else {
currentCue.text = line;
}
}
}
// 添加最后一个cue
if (currentCue) {
cues.push(currentCue);
}
return {
metadata: {
note: note,
style: style,
region: region
},
cues: cues
};
}
}]);
}(); // 解析脚本信息
_defineProperty(YbSubtitle, "DEFAULT_CONFIG", {
color: '#ffffff',
//文字颜色
fontSize: 18,
//字体大小
bottom: '10%',
//底部边距
shadowColor: 'rgba(0,0,0,.5)' //阴影颜色
});
function parseScriptInfo(line, infoObj) {
var colonIndex = line.indexOf(':');
if (colonIndex === -1) return;
var key = line.substring(0, colonIndex).trim();
var value = line.substring(colonIndex + 1).trim();
infoObj[key] = value;
}
// 解析样式
function parseStyles(line, stylesObj) {
if (line.toLowerCase().startsWith('format:')) {
// 解析格式行
var formatLine = line.substring(7).trim();
stylesObj.format = formatLine.split(',').map(function (item) {
return item.trim();
});
} else if (line.toLowerCase().startsWith('style:')) {
// 解析样式数据
var styleLine = line.substring(6).trim();
var values = parseCsvLine(styleLine);
if (values.length === stylesObj.format.length) {
var style = {};
stylesObj.format.forEach(function (key, index) {
style[key] = values[index];
});
stylesObj.data.push(style);
}
}
}
// 解析事件
function parseEvents(line, eventsObj) {
if (line.toLowerCase().startsWith('format:')) {
// 解析格式行
var formatLine = line.substring(7).trim();
eventsObj.format = formatLine.split(',').map(function (item) {
return item.trim();
});
} else if (line.toLowerCase().startsWith('dialogue:') || line.toLowerCase().startsWith('comment:')) {
// 解析对话或注释数据
var typeEnd = line.indexOf(':');
var type = line.substring(0, typeEnd).trim();
var dataLine = line.substring(typeEnd + 1).trim();
var values = parseCsvLine(dataLine);
if (values.length === eventsObj.format.length) {
var event = {
Type: type
};
eventsObj.format.forEach(function (key, index) {
event[key] = values[index];
});
eventsObj.data.push(event);
}
}
}
// 解析CSV格式的行处理逗号在引号内的情况
function parseCsvLine(line) {
var result = [];
var current = '';
var inQuotes = false;
for (var i = 0; i < line.length; i++) {
var _char = line[i];
if (_char === '"') {
inQuotes = !inQuotes;
} else if (_char === ',' && !inQuotes) {
result.push(current.trim());
current = '';
} else {
current += _char;
}
}
result.push(current.trim());
return result;
}
// 解析Cue设置
function parseCueSettings(settingsLine) {
var settings = {};
var parts = settingsLine.split(' ');
var _iterator = _createForOfIteratorHelper(parts),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var part = _step.value;
var _part$split = part.split(':'),
_part$split2 = _slicedToArray(_part$split, 2),
key = _part$split2[0],
value = _part$split2[1];
if (key && value) {
settings[key] = value;
}
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
return settings;
}
//兼容new Function为了挂载到window对象上
if (typeof window != 'undefined') {
window.YbSubtitle = YbSubtitle;
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,134 @@
/**
* @author richt / http://richt.me
* @author WestLangley / http://github.com/WestLangley
*
* W3C Device Orientation control (http://w3c.github.io/deviceorientation/spec-source-orientation.html)
*/
THREE.DeviceOrientationControls = function ( object ) {
var scope = this;
this.object = object;
this.object.rotation.reorder( 'YXZ' );
this.enabled = true;
this.deviceOrientation = {};
this.screenOrientation = 0;
this.alphaOffset = 0; // radians
var onDeviceOrientationChangeEvent = function ( event ) {
scope.deviceOrientation = event;
};
var onScreenOrientationChangeEvent = function () {
scope.screenOrientation = window.orientation || 0;
};
// The angles alpha, beta and gamma form a set of intrinsic Tait-Bryan angles of type Z-X'-Y''
var setObjectQuaternion = function () {
var zee = new THREE.Vector3( 0, 0, 1 );
var euler = new THREE.Euler();
var q0 = new THREE.Quaternion();
var q1 = new THREE.Quaternion( - Math.sqrt( 0.5 ), 0, 0, Math.sqrt( 0.5 ) ); // - PI/2 around the x-axis
return function ( quaternion, alpha, beta, gamma, orient ) {
euler.set( beta, alpha, - gamma, 'YXZ' ); // 'ZXY' for the device, but 'YXZ' for us
quaternion.setFromEuler( euler ); // orient the device
quaternion.multiply( q1 ); // camera looks out the back of the device, not the top
quaternion.multiply( q0.setFromAxisAngle( zee, - orient ) ); // adjust for screen orientation
};
}();
this.connect = function () {
onScreenOrientationChangeEvent(); // run once on load
// iOS 13+
if ( window.DeviceOrientationEvent !== undefined && typeof window.DeviceOrientationEvent.requestPermission === 'function' ) {
window.DeviceOrientationEvent.requestPermission().then( function ( response ) {
if ( response == 'granted' ) {
window.addEventListener( 'orientationchange', onScreenOrientationChangeEvent, false );
window.addEventListener( 'deviceorientation', onDeviceOrientationChangeEvent, false );
}
} ).catch( function ( error ) {
console.error( 'THREE.DeviceOrientationControls: Unable to use DeviceOrientation API:', error );
} );
} else {
window.addEventListener( 'orientationchange', onScreenOrientationChangeEvent, false );
window.addEventListener( 'deviceorientation', onDeviceOrientationChangeEvent, false );
}
scope.enabled = true;
};
this.disconnect = function () {
window.removeEventListener( 'orientationchange', onScreenOrientationChangeEvent, false );
window.removeEventListener( 'deviceorientation', onDeviceOrientationChangeEvent, false );
scope.enabled = false;
};
this.update = function () {
if ( scope.enabled === false ) return;
var device = scope.deviceOrientation;
if ( device ) {
var alpha = device.alpha ? THREE.MathUtils.degToRad( device.alpha ) + scope.alphaOffset : 0; // Z
var beta = device.beta ? THREE.MathUtils.degToRad( device.beta ) : 0; // X'
var gamma = device.gamma ? THREE.MathUtils.degToRad( device.gamma ) : 0; // Y''
var orient = scope.screenOrientation ? THREE.MathUtils.degToRad( scope.screenOrientation ) : 0; // O
setObjectQuaternion( scope.object.quaternion, alpha, beta, gamma, orient );
}
};
this.dispose = function () {
scope.disconnect();
};
this.connect();
};

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,284 @@
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover">
<script type="text/javascript" src="js/hls.min.js"></script>
<script type="text/javascript" src="js/flv.min.js"></script>
<script type="text/javascript" src="js/jsmpeg.min.js"></script>
<script type="text/javascript" src="js/three.min.js"></script>
<script type="text/javascript" src="js/OrbitControls.js"></script>
<script type="text/javascript" src="js/DeviceOrientationControls.js"></script>
<script type="text/javascript" src="js/uni-webview-js@1.5.4.js"></script>
<script type="text/javascript" src="dist/yb-player-gesture.js"></script>
<script type="text/javascript" src="dist/yb-player-subtitle.js"></script>
<script type="text/javascript" src="dist/yb-player-danmu.js"></script>
<script type="text/javascript" src="dist/yb-player-pano.js"></script>
<script type="text/javascript" src="dist/yb-player-mpeg.js"></script>
<script type="text/javascript" src="dist/yb-player.js"></script>
<link rel="stylesheet" type="text/css" href="css/yb-player.css"/>
<link rel="stylesheet" type="text/css" href="css/yb-player-plugin.css"/>
<title>全局播放器</title>
<style>
html, body {
padding: 0;
margin: 0;
background-color: #000;
width: 100vw;
height: 100vh;
}
.btns {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: none;
flex-direction: row;
}
.yb-player {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<!-- Specify the player container -->
<div id="yb-player" class="yb-player">
</div>
<script>
var mp
//加载视频
function reloadVideo (arg) {
var params = parseArg(arg)
unload()
_traverseObject(params.custom, 'slotclick')
mp = new YbPlayer({
container:'#yb-player',
src: params.src,
segments: params.segments,
title: params.title,
poster: params.poster || undefined,
type: params.type,
three: params.three,
initialTime: params.initialTime,
duration: params.duration,
autoplay: params.autoplay,
preload: params.preload,
muted: params.muted,
playbackRate: params.playbackRate,
loop: params.loop,
isLive: params.isLive,
header: params.header,
controls: params.controls,
height: '100%',
objectFit: params.objectFit,
crossOrigin: params.crossOrigin,
openDirection: params.openDirection,
exitDirection: params.exitDirection,
quality: params.quality,
works: params.works,
workIndex: params.workIndex,
subtitles: params.subtitles,
subtitleIndex: params.subtitleIndex,
custom: params.custom,
decoder: {
hls: {
loader: Hls,
config: parseHlsConfig(params.hlsConfig)
},
flv: {
loader: flvjs,
config: params.flvConfig
},
jsmpeg: {
loader: JSMpeg,
config: params.jsmpegConfig
}
}
})
mp.load()
mp.loadVideo()
mp.loadGestureEvent()
mp.onmessage = function (data) {
uni.postMessage({data})
}
window.addEventListener('resize', updateSize)
}
function updateSize () {
if( mp ) {
mp.refreshDanmu()
mp.refreshPano()
}
}
//动态修改video属性
function setVideo (key, value) {
if( mp ) mp.setVideo(key, value)
}
//加载弹幕
function loadDanmu () {
if ( mp ) {
if ( window.danmu && window.danmu.length ) mp.setConfig('danmu', window.danmu)
mp.unloadDanmu()
mp.loadDanmu()
window.dammu = null
}
}
//卸载弹幕
function unloadDanmu () {
if ( mp ) mp.unloadDanmu()
}
//发送弹幕
function sendDanmu (arg, border) {
var danmu = parseArg(arg)
if ( mp ) mp.sendDanmu(danmu, border)
}
//插入弹幕
function insertDanmu (arg) {
var danmu = parseArg(arg)
if ( mp ) mp.insertDanmu(danmu)
}
//更新配置
function updateConfig (arg) {
var config = parseArg(arg)
if ( mp ) {
Object.keys(config).forEach(key => {
mp.setConfig(key, config[key])
})
mp.hideControls()
}
}
//重加载自定义配置
function reloadCustom (arg) {
var config = parseArg(arg)
_traverseObject(config, 'slotclick')
if ( mp ) {
Object.keys(config).forEach(key => {
mp.setCustom(key, config[key])
})
mp.unloadCustom()
mp.loadCustom()
}
}
//播放/暂停
function toggle () {
if ( mp ) mp.toggle()
}
//播放
function play () {
if ( mp ) mp.video.play()
}
//暂停
function pause () {
if ( mp ) mp.video.pause()
}
//跳转
function seek (time) {
if ( mp ) mp.seek(time)
}
//开启全屏
function openFullscreen (direction) {
if ( mp ) mp.openFullscreen(direction)
}
//退出全屏
function exitFullscreen () {
if ( mp ) mp.exitFullscreen()
}
//消息提示
function showToast (arg) {
var data = parseArg(arg)
if ( mp ) mp.showToast(data)
}
//展示工具栏
function showToolbar (arg) {
var data = parseArg(arg)
_traverseObject(data, 'toolclick')
if ( mp ) mp.showToolbar(data.selector, data.list, data.checkShow, data.checkIndex)
}
//截图
function capture (arg) {
var data = parseArg(arg)
if ( mp ) mp.capture(data.type, data.show)
}
//禁用手势事件
function disableGesture () {
if ( mp ) mp.disableGesture()
}
//启用手势事件
function enableGesture () {
if ( mp ) mp.enableGesture()
}
//卸载视频
function unload () {
if ( mp ) {
mp.unloadDanmu()
mp.unloadGestureEvent()
mp.unloadVideo()
mp.unload()
window.removeEventListener('resize', updateSize)
}
}
//卸载视频
function destroy () {
unload()
uni.postMessage({data: {destroyed: true}})
}
//处理custom
function _traverseObject(obj, emitname) {
if (typeof obj !== 'object' || obj === null) {
return;
}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
if (key == 'click') {
var emit = {}
emit[emitname] = value
obj[key] = function () { uni.postMessage({data: emit}) }//点击事件通知
} else if (typeof value === 'object' && value !== null) {
_traverseObject(value, emitname);
}
}
}
}
//处理hlsConfig
function parseHlsConfig (config = {}) {
Object.keys(config).forEach(key => {
if ( ['xhrSetup'].includes(key) ) {
config[key] = eval(`(${config[key]})`);
}
if ( ['pLoader', 'fLoader'].includes(key) ) {
config[key] = (new Function(`return ${config[key]}`))();
}
})
return config
}
//设置弹幕数据webview专用
function setDanmuData (arg) {
var { code, data } = parseArg(arg)
if ( !window.danmu ) window.danmu = []
window.danmu = window.danmu.concat(data)
if ( code == 1 ) loadDanmu()
}
//转义参数
function parseArg (arg) {
try{
return JSON.parse(decodeURIComponent(decodeURIComponent(arg)))
}catch(e){
return arg
}
}
//通知app已经做好准备
function ready () {
uni.postMessage({data: {ready: true}})
}
if (document.readyState === 'complete') {
// 如果页面已经加载完成,直接执行函数
ready();
} else {
window.addEventListener('load', ready)
}
</script>
</body>
</html>