平安校园
25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.

DomVideoPlayer.vue 15 KiB

2 ay önce
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. <!-- eslint-disable -->
  2. <template>
  3. <view
  4. class="player-wrapper"
  5. :id="videoWrapperId"
  6. :parentId="id"
  7. :randomNum="randomNum"
  8. :change:randomNum="domVideoPlayer.randomNumChange"
  9. :viewportProps="viewportProps"
  10. :change:viewportProps="domVideoPlayer.viewportChange"
  11. :videoSrc="videoSrc"
  12. :change:videoSrc="domVideoPlayer.initVideoPlayer"
  13. :command="eventCommand"
  14. :change:command="domVideoPlayer.triggerCommand"
  15. :func="renderFunc"
  16. :change:func="domVideoPlayer.triggerFunc"
  17. />
  18. </template>
  19. <script>
  20. export default {
  21. props: {
  22. src: {
  23. type: String,
  24. default: ''
  25. },
  26. autoplay: {
  27. type: Boolean,
  28. default: false
  29. },
  30. loop: {
  31. type: Boolean,
  32. default: false
  33. },
  34. controls: {
  35. type: Boolean,
  36. default: false
  37. },
  38. objectFit: {
  39. type: String,
  40. default: 'contain'
  41. },
  42. muted: {
  43. type: Boolean,
  44. default: false
  45. },
  46. playbackRate: {
  47. type: Number,
  48. default: 1
  49. },
  50. isLoading: {
  51. type: Boolean,
  52. default: false
  53. },
  54. poster: {
  55. type: String,
  56. default: ''
  57. },
  58. id: {
  59. type: String,
  60. default: ''
  61. }
  62. },
  63. data() {
  64. return {
  65. randomNum: Math.floor(Math.random() * 100000000),
  66. videoSrc: '',
  67. // 父组件向子组件传递的事件指令(video的原生事件)
  68. eventCommand: null,
  69. // 父组件传递过来的,对 renderjs 层的函数执行(对视频控制的自定义事件)
  70. renderFunc: {
  71. name: null,
  72. params: null
  73. },
  74. // 提供给父组件进行获取的视频属性
  75. currentTime: 0,
  76. duration: 0,
  77. playing: false
  78. }
  79. },
  80. watch: {
  81. // 监听视频资源地址更新
  82. src: {
  83. handler(val) {
  84. if (!val) return
  85. setTimeout(() => {
  86. this.videoSrc = val
  87. }, 0)
  88. },
  89. immediate: true
  90. }
  91. },
  92. computed: {
  93. videoWrapperId() {
  94. return `video-wrapper-${this.randomNum}`
  95. },
  96. // 聚合视图层的所有数据变化,传给renderjs的渲染层
  97. viewportProps() {
  98. return {
  99. autoplay: this.autoplay,
  100. muted: this.muted,
  101. controls: this.controls,
  102. loop: this.loop,
  103. objectFit: this.objectFit,
  104. poster: this.poster,
  105. isLoading: this.isLoading,
  106. playbackRate: this.playbackRate
  107. }
  108. }
  109. },
  110. // 方法
  111. methods: {
  112. // 传递事件指令给父组件
  113. eventEmit({ event, data }) {
  114. this.$emit(event, data)
  115. },
  116. // 修改view视图层的data数据
  117. setViewData({ key, value }) {
  118. key && this.$set(this, key, value)
  119. },
  120. // 重置事件指令
  121. resetEventCommand() {
  122. this.eventCommand = null
  123. },
  124. // 播放指令
  125. play() {
  126. this.eventCommand = 'play'
  127. },
  128. // 暂停指令
  129. pause() {
  130. this.eventCommand = 'pause'
  131. },
  132. // 重置自定义函数指令
  133. resetFunc() {
  134. this.renderFunc = {
  135. name: null,
  136. params: null
  137. }
  138. },
  139. // 自定义函数 - 移除视频
  140. remove(params) {
  141. this.renderFunc = {
  142. name: 'removeHandler',
  143. params
  144. }
  145. },
  146. // 自定义函数 - 全屏播放
  147. fullScreen(params) {
  148. this.renderFunc = {
  149. name: 'fullScreenHandler',
  150. params
  151. }
  152. },
  153. // 自定义函数 - 跳转到指定时间点
  154. toSeek(sec, isDelay = false) {
  155. this.renderFunc = {
  156. name: 'toSeekHandler',
  157. params: { sec, isDelay }
  158. }
  159. }
  160. }
  161. }
  162. </script>
  163. <script module="domVideoPlayer" lang="renderjs">
  164. const PLAYER_ID = 'DOM_VIDEO_PLAYER'
  165. export default {
  166. data() {
  167. return {
  168. num: '',
  169. videoEl: null,
  170. loadingEl: null,
  171. // 延迟生效的函数
  172. delayFunc: null,
  173. renderProps: {}
  174. }
  175. },
  176. computed: {
  177. playerId() {
  178. return `${PLAYER_ID}_${this.num}`
  179. },
  180. wrapperId() {
  181. return `video-wrapper-${this.num}`
  182. }
  183. },
  184. methods: {
  185. isApple() {
  186. const ua = navigator.userAgent.toLowerCase()
  187. return ua.indexOf('iphone') !== -1 || ua.indexOf('ipad') !== -1
  188. },
  189. async initVideoPlayer(src) {
  190. this.delayFunc = null
  191. await this.$nextTick()
  192. if (!src) return
  193. if (this.videoEl) {
  194. // 切换视频源
  195. if (!this.isApple() && this.loadingEl) {
  196. this.loadingEl.style.display = 'block'
  197. }
  198. this.videoEl.src = src
  199. return
  200. }
  201. const videoEl = document.createElement('video')
  202. this.videoEl = videoEl
  203. // 开始监听视频相关事件
  204. this.listenVideoEvent()
  205. const { autoplay, muted, controls, loop, playbackRate, objectFit, poster } = this.renderProps
  206. videoEl.src = src
  207. videoEl.autoplay = autoplay
  208. videoEl.controls = controls
  209. videoEl.loop = loop
  210. videoEl.muted = muted
  211. videoEl.playbackRate = playbackRate
  212. videoEl.id = this.playerId
  213. // videoEl.setAttribute('x5-video-player-type', 'h5')
  214. videoEl.setAttribute('preload', 'auto')
  215. videoEl.setAttribute('playsinline', true)
  216. videoEl.setAttribute('webkit-playsinline', true)
  217. videoEl.setAttribute('crossorigin', 'anonymous')
  218. videoEl.setAttribute('controlslist', 'nodownload')
  219. videoEl.setAttribute('disablePictureInPicture', true)
  220. videoEl.style.objectFit = objectFit
  221. poster && (videoEl.poster = poster)
  222. videoEl.style.width = '100%'
  223. videoEl.style.height = '100%'
  224. // 插入视频元素
  225. // document.getElementById(this.wrapperId).appendChild(videoEl)
  226. const playerWrapper = document.getElementById(this.wrapperId)
  227. playerWrapper.insertBefore(videoEl, playerWrapper.firstChild)
  228. // 插入loading 元素(遮挡安卓的默认加载过程中的黑色播放按钮)
  229. this.createLoading()
  230. },
  231. // 创建 loading
  232. createLoading() {
  233. const { isLoading } = this.renderProps
  234. if (!this.isApple() && isLoading) {
  235. const loadingEl = document.createElement('div')
  236. this.loadingEl = loadingEl
  237. loadingEl.className = 'loading-wrapper'
  238. loadingEl.style.position = 'absolute'
  239. loadingEl.style.top = '0'
  240. loadingEl.style.left = '0'
  241. loadingEl.style.zIndex = '1'
  242. loadingEl.style.width = '100%'
  243. loadingEl.style.height = '100%'
  244. loadingEl.style.backgroundColor = 'black'
  245. document.getElementById(this.wrapperId).appendChild(loadingEl)
  246. // 创建 loading 动画
  247. const animationEl = document.createElement('div')
  248. animationEl.className = 'loading'
  249. animationEl.style.zIndex = '2'
  250. animationEl.style.position = 'absolute'
  251. animationEl.style.top = '50%'
  252. animationEl.style.left = '50%'
  253. animationEl.style.marginTop = '-15px'
  254. animationEl.style.marginLeft = '-15px'
  255. animationEl.style.width = '30px'
  256. animationEl.style.height = '30px'
  257. animationEl.style.border = '2px solid #FFF'
  258. animationEl.style.borderTopColor = 'rgba(255, 255, 255, 0.2)'
  259. animationEl.style.borderRightColor = 'rgba(255, 255, 255, 0.2)'
  260. animationEl.style.borderBottomColor = 'rgba(255, 255, 255, 0.2)'
  261. animationEl.style.borderRadius = '100%'
  262. animationEl.style.animation = 'circle infinite 0.75s linear'
  263. loadingEl.appendChild(animationEl)
  264. // 创建 loading 动画所需的 keyframes
  265. const style = document.createElement('style')
  266. const keyframes = `
  267. @keyframes circle {
  268. 0% {
  269. transform: rotate(0);
  270. }
  271. 100% {
  272. transform: rotate(360deg);
  273. }
  274. }
  275. `
  276. style.type = 'text/css'
  277. if (style.styleSheet) {
  278. style.styleSheet.cssText = keyframes
  279. } else {
  280. style.appendChild(document.createTextNode(keyframes))
  281. }
  282. document.head.appendChild(style)
  283. }
  284. },
  285. // 监听视频相关事件
  286. listenVideoEvent() {
  287. // 播放事件监听
  288. const playHandler = () => {
  289. this.$ownerInstance.callMethod('eventEmit', { event: 'play' })
  290. this.$ownerInstance.callMethod('setViewData', {
  291. key: 'playing',
  292. value: true
  293. })
  294. if (this.loadingEl) {
  295. this.loadingEl.style.display = 'none'
  296. }
  297. }
  298. this.videoEl.removeEventListener('play', playHandler)
  299. this.videoEl.addEventListener('play', playHandler)
  300. // 暂停事件监听
  301. const pauseHandler = () => {
  302. this.$ownerInstance.callMethod('eventEmit', { event: 'pause' })
  303. this.$ownerInstance.callMethod('setViewData', {
  304. key: 'playing',
  305. value: false
  306. })
  307. }
  308. this.videoEl.removeEventListener('pause', pauseHandler)
  309. this.videoEl.addEventListener('pause', pauseHandler)
  310. // 结束事件监听
  311. const endedHandler = () => {
  312. this.$ownerInstance.callMethod('eventEmit', { event: 'ended' })
  313. this.$ownerInstance.callMethod('resetEventCommand')
  314. }
  315. this.videoEl.removeEventListener('ended', endedHandler)
  316. this.videoEl.addEventListener('ended', endedHandler)
  317. // 加载完成事件监听
  318. const canPlayHandler = () => {
  319. this.$ownerInstance.callMethod('eventEmit', { event: 'canplay' })
  320. this.execDelayFunc()
  321. }
  322. this.videoEl.removeEventListener('canplay', canPlayHandler)
  323. this.videoEl.addEventListener('canplay', canPlayHandler)
  324. // 加载失败事件监听
  325. const errorHandler = (e) => {
  326. if (this.loadingEl) {
  327. this.loadingEl.style.display = 'block'
  328. }
  329. this.$ownerInstance.callMethod('eventEmit', { event: 'error' })
  330. }
  331. this.videoEl.removeEventListener('error', errorHandler)
  332. this.videoEl.addEventListener('error', errorHandler)
  333. // loadedmetadata 事件监听
  334. const loadedMetadataHandler = () => {
  335. this.$ownerInstance.callMethod('eventEmit', { event: 'loadedmetadata' })
  336. // 获取视频的长度
  337. const duration = this.videoEl.duration
  338. this.$ownerInstance.callMethod('eventEmit', {
  339. event: 'durationchange',
  340. data: duration
  341. })
  342. this.$ownerInstance.callMethod('setViewData', {
  343. key: 'duration',
  344. value: duration
  345. })
  346. // 加载首帧视频 模拟出封面图
  347. this.loadFirstFrame()
  348. }
  349. this.videoEl.removeEventListener('loadedmetadata', loadedMetadataHandler)
  350. this.videoEl.addEventListener('loadedmetadata', loadedMetadataHandler)
  351. // 播放进度监听
  352. const timeupdateHandler = (e) => {
  353. const currentTime = e.target.currentTime
  354. this.$ownerInstance.callMethod('eventEmit', {
  355. event: 'timeupdate',
  356. data: currentTime
  357. })
  358. this.$ownerInstance.callMethod('setViewData', {
  359. key: 'currentTime',
  360. value: currentTime
  361. })
  362. }
  363. this.videoEl.removeEventListener('timeupdate', timeupdateHandler)
  364. this.videoEl.addEventListener('timeupdate', timeupdateHandler)
  365. // 倍速播放监听
  366. const ratechangeHandler = (e) => {
  367. const playbackRate = e.target.playbackRate
  368. this.$ownerInstance.callMethod('eventEmit', {
  369. event: 'ratechange',
  370. data: playbackRate
  371. })
  372. }
  373. this.videoEl.removeEventListener('ratechange', ratechangeHandler)
  374. this.videoEl.addEventListener('ratechange', ratechangeHandler)
  375. // 全屏事件监听
  376. if (this.isApple()) {
  377. const webkitbeginfullscreenHandler = () => {
  378. const presentationMode = this.videoEl.webkitPresentationMode
  379. let isFullScreen = null
  380. if (presentationMode === 'fullscreen') {
  381. isFullScreen = true
  382. } else {
  383. isFullScreen = false
  384. }
  385. this.$ownerInstance.callMethod('eventEmit', {
  386. event: 'fullscreenchange',
  387. data: isFullScreen
  388. })
  389. }
  390. this.videoEl.removeEventListener('webkitpresentationmodechanged', webkitbeginfullscreenHandler)
  391. this.videoEl.addEventListener('webkitpresentationmodechanged', webkitbeginfullscreenHandler)
  392. } else {
  393. const fullscreenchangeHandler = () => {
  394. let isFullScreen = null
  395. if (document.fullscreenElement) {
  396. isFullScreen = true
  397. } else {
  398. isFullScreen = false
  399. }
  400. this.$ownerInstance.callMethod('eventEmit', {
  401. event: 'fullscreenchange',
  402. data: isFullScreen
  403. })
  404. }
  405. document.removeEventListener('fullscreenchange', fullscreenchangeHandler)
  406. document.addEventListener('fullscreenchange', fullscreenchangeHandler)
  407. }
  408. },
  409. // 加载首帧视频,模拟出封面图
  410. loadFirstFrame() {
  411. let { autoplay, muted } = this.renderProps
  412. if (this.isApple()) {
  413. this.videoEl.play()
  414. if (!autoplay) {
  415. this.videoEl.pause()
  416. }
  417. } else {
  418. // optimize: timeout 延迟调用是为了规避控制台的`https://goo.gl/LdLk22`这个报错
  419. /**
  420. * 原因:chromium 内核中,谷歌协议规定,视频不允许在非静音状态下进行自动播放
  421. * 解决:在自动播放时,先将视频静音,然后延迟调用 play 方法,播放视频
  422. * 说明:iOS 的 Safari 内核不会有这个,仅在 Android 设备出现,即使有这个报错也不影响的,所以不介意控制台报错的话是可以删掉这个 timeout 的
  423. */
  424. this.videoEl.muted = true
  425. setTimeout(() => {
  426. this.videoEl.play()
  427. this.videoEl.muted = muted
  428. if (!autoplay) {
  429. setTimeout(() => {
  430. this.videoEl.pause()
  431. }, 100)
  432. }
  433. }, 10)
  434. }
  435. },
  436. triggerCommand(eventType) {
  437. if (eventType) {
  438. this.$ownerInstance.callMethod('resetEventCommand')
  439. this.videoEl && this.videoEl[eventType]()
  440. }
  441. },
  442. triggerFunc(func) {
  443. const { name, params } = func || {}
  444. if (name) {
  445. this[name](params)
  446. this.$ownerInstance.callMethod('resetFunc')
  447. }
  448. },
  449. removeHandler() {
  450. if (this.videoEl) {
  451. this.videoEl.pause()
  452. this.videoEl.src = ''
  453. this.$ownerInstance.callMethod('setViewData', {
  454. key: 'videoSrc',
  455. value: ''
  456. })
  457. this.videoEl.load()
  458. }
  459. },
  460. fullScreenHandler() {
  461. if (this.isApple()) {
  462. this.videoEl.webkitEnterFullscreen()
  463. } else {
  464. this.videoEl.requestFullscreen()
  465. }
  466. },
  467. toSeekHandler({ sec, isDelay }) {
  468. const func = () => {
  469. if (this.videoEl) {
  470. this.videoEl.currentTime = sec
  471. }
  472. }
  473. // 延迟执行
  474. if (isDelay) {
  475. this.delayFunc = func
  476. } else {
  477. func()
  478. }
  479. },
  480. // 执行延迟函数
  481. execDelayFunc() {
  482. this.delayFunc && this.delayFunc()
  483. this.delayFunc = null
  484. },
  485. viewportChange(props) {
  486. this.renderProps = props
  487. const { autoplay, muted, controls, loop, playbackRate } = props
  488. if (this.videoEl) {
  489. this.videoEl.autoplay = autoplay
  490. this.videoEl.controls = controls
  491. this.videoEl.loop = loop
  492. this.videoEl.muted = muted
  493. this.videoEl.playbackRate = playbackRate
  494. }
  495. },
  496. randomNumChange(val) {
  497. this.num = val
  498. }
  499. }
  500. }
  501. </script>
  502. <style scoped>
  503. .player-wrapper {
  504. overflow: hidden;
  505. height: 100%;
  506. padding: 0;
  507. position: relative;
  508. }
  509. </style>