index.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. import { defineComponent, PropType } from 'vue'
  2. import styles from './index.module.less'
  3. import Plyr from 'plyr'
  4. import 'plyr/dist/plyr.css'
  5. import { Button, Icon, Loading, Toast } from 'vant'
  6. import TCPlayer from 'tcplayer.js'
  7. import 'tcplayer.js/dist/tcplayer.css'
  8. import iconVideoPlay from '@/common/images/icon_video_play.png'
  9. import { browser } from '@/helpers/utils'
  10. import { state } from '@/state'
  11. import { listenerMessage, postMessage } from '@/helpers/native-message'
  12. export default defineComponent({
  13. name: 'col-video',
  14. props: {
  15. trySee: {
  16. // 是否试看
  17. type: Boolean,
  18. default: false
  19. },
  20. freeTitleStatus: {
  21. type: Boolean,
  22. default: true
  23. },
  24. // 试看比例
  25. freeRate: {
  26. type: Number,
  27. default: 100
  28. },
  29. setting: {
  30. type: Object,
  31. default: () => {}
  32. },
  33. controls: Boolean,
  34. height: String,
  35. src: {
  36. type: String,
  37. default: ''
  38. },
  39. poster: {
  40. type: String,
  41. default: ''
  42. },
  43. styleValue: {
  44. type: Object,
  45. default: () => ({})
  46. },
  47. preload: {
  48. type: String as PropType<'auto' | 'metadata' | 'none'>,
  49. default: 'auto'
  50. },
  51. currentTime: {
  52. type: Boolean,
  53. default: true
  54. },
  55. playsinline: {
  56. type: Boolean,
  57. default: true
  58. },
  59. onPlay: {
  60. type: Function,
  61. default: () => {}
  62. },
  63. isBuy: {
  64. // 是否把购买方法在外部调用
  65. type: Boolean,
  66. default: false
  67. },
  68. onBuyEmit: {
  69. type: Function,
  70. default: () => {}
  71. }
  72. },
  73. data() {
  74. return {
  75. videoID: 'video' + Date.now() + Math.floor(Math.random() * 100),
  76. player: null as any,
  77. // playTime: 0,
  78. loading: true, // 首次进入加载中
  79. trySeeOver: false, // 试看是否结束
  80. showSeeStatus: true // 是否显示试看状态
  81. }
  82. },
  83. mounted() {
  84. this._init()
  85. listenerMessage('setVideoPlayer', result => {
  86. const content = result?.content
  87. if (content.status === 'pause') {
  88. this.player.pause()
  89. }
  90. })
  91. },
  92. beforeUnmount() {
  93. postMessage({
  94. api: 'limitScreenRecord',
  95. content: {
  96. type: 0
  97. }
  98. })
  99. },
  100. computed: {
  101. computedSeeStatus() {
  102. // console.log(
  103. // this.showSeeStatus,
  104. // this.trySee,
  105. // this.trySeeOver,
  106. // 'this.showSeeStatus, this.trySee'
  107. // )
  108. return this.showSeeStatus && this.trySee
  109. },
  110. playTime() {
  111. // 允许播放时间
  112. const player = this.player
  113. const playTime = (player.duration() * this.freeRate) / 100
  114. return playTime || 0
  115. }
  116. },
  117. methods: {
  118. _init() {
  119. // const controls = [
  120. // 'play-large',
  121. // 'play',
  122. // 'progress',
  123. // 'captions',
  124. // 'fullscreen'
  125. // ]
  126. // if (this.currentTime) {
  127. // controls.push('current-time')
  128. // }
  129. // const params: any = {
  130. // controls: controls,
  131. // ...this.setting,
  132. // invertTime: false
  133. // }
  134. // if (browser().iPhone) {
  135. // params.fullscreen = {
  136. // enabled: true,
  137. // fallback: 'force',
  138. // iosNative: true
  139. // }
  140. // }
  141. // this.player = new Plyr((this as any).$refs.video, params)
  142. // this.player.elements.container
  143. // ? (this.player.elements.container.style.height = this.height || '210px')
  144. // : null
  145. // if (this.preload === 'none') {
  146. // this.loading = false
  147. // }
  148. // this.player.on('loadedmetadata', () => {
  149. // this.loading = false
  150. // if (this.trySee) {
  151. // this.domPlayVisibility()
  152. // } else {
  153. // this.domPlayVisibility(false)
  154. // }
  155. // // 监听播放事件
  156. // const _this = this
  157. // this.player.on('timeupdate', () => {
  158. // const players = _this.player
  159. // if (players.currentTime >= this.playTime && _this.trySee) {
  160. // players.pause()
  161. // _this.trySeeOver = true
  162. // _this.showSeeStatus = true
  163. // _this.domPlayVisibility() // 试看结束后隐藏播放按钮
  164. // }
  165. // })
  166. // })
  167. // this.player.on('play', () => {
  168. // postMessage(
  169. // {
  170. // api: 'getDeviceStatus',
  171. // content: {
  172. // type: 'video'
  173. // }
  174. // },
  175. // (res: any) => {
  176. // // 判断是否在录屏中, 如果在录屏则不允许播放
  177. // if (res.content.status == '1') {
  178. // Toast('为了保证数据安全,请不要录屏')
  179. // this.player.pause()
  180. // }
  181. // }
  182. // )
  183. // postMessage({
  184. // api: 'limitScreenRecord',
  185. // content: {
  186. // type: 1
  187. // }
  188. // })
  189. // this.onPlay && this.onPlay()
  190. // })
  191. // this.player.on('enterfullscreen', () => {
  192. // console.log('fullscreen')
  193. // const i = document.createElement('i')
  194. // i.id = 'fullscreen-back'
  195. // i.className = 'van-icon van-icon-arrow-left video-back'
  196. // i.addEventListener('click', () => {
  197. // this.player.fullscreen.exit()
  198. // })
  199. // console.log(document.getElementsByClassName('plyr'))
  200. // document.getElementsByClassName('plyr')[0].appendChild(i)
  201. // })
  202. // this.player.on('exitfullscreen', () => {
  203. // console.log('exitfullscreen')
  204. // const i = document.getElementById('fullscreen-back')
  205. // i && i.remove()
  206. // })
  207. const Button = TCPlayer.getComponent('Button')
  208. const BigPlayButton = TCPlayer.getComponent('BigPlayButton')
  209. BigPlayButton.prototype.createEl = function () {
  210. const el = Button.prototype.createEl.call(this)
  211. const _html =
  212. '<button><svg width="41px"height="41px"viewBox="0 0 41 41"version="1.1"xmlns="http://www.w3.org/2000/svg"xmlns:xlink="http://www.w3.org/1999/xlink"><g stroke="none"stroke-width="1"fill="none"fill-rule="evenodd"><g transform="translate(-167.000000, -155.000000)"><g transform="translate(0.000000, 85.000000)"><g transform="translate(158.000000, 70.000000)"><g transform="translate(9.000000, 0.000000)"><circle id="椭圆形"stroke="#FFFFFF"fill-opacity="0.1"fill="#D8D8D8"cx="20.5"cy="20.5"r="20"></circle><path d="M14.5483871,27.6859997 L14.5483871,13.4342349 C14.5480523,12.8729571 14.8729597,12.356555 15.3949624,12.0887034 C15.9169651,11.8208518 16.5522696,11.8445472 17.0503046,12.1504437 L28.6530473,19.2778563 C29.1119763,19.5602271 29.3887725,20.0426422 29.3887725,20.5601173 C29.3887725,21.0775924 29.1119763,21.5600075 28.6530473,21.8423783 L17.0503046,28.9697909 C16.5522696,29.2756874 15.9169651,29.2993828 15.3949624,29.0315312 C14.8729597,28.7636796 14.5480523,28.2472775 14.5483871,27.6859997 Z"id="路径"fill="#FFFFFF"fill-rule="nonzero"></path></g></g></g></g></g></svg></button>'
  213. el.appendChild(
  214. TCPlayer.dom.createEl('div', {
  215. className: 'vjs-button-icon',
  216. innerHTML: _html
  217. })
  218. )
  219. return el
  220. }
  221. this.player = TCPlayer(this.videoID, {
  222. appID: '',
  223. controls: true
  224. }) // player-container-id 为播放器容器 ID,必须与 html 中一致
  225. if (this.player) {
  226. this.player.src(this.src) // url 播放地址
  227. this.player.poster(this.poster || '')
  228. if (this.preload === 'none') {
  229. this.loading = false
  230. }
  231. this.player.on('loadstart', () => {
  232. this.loading = false
  233. if (this.trySee) {
  234. this.domPlayVisibility()
  235. } else {
  236. this.domPlayVisibility(false)
  237. }
  238. // 监听播放事件
  239. this.player.on('timeupdate', () => {
  240. const players = this.player
  241. if (players.currentTime() >= this.playTime && this.trySee) {
  242. players.pause()
  243. this.trySeeOver = true
  244. this.showSeeStatus = true
  245. this.domPlayVisibility() // 试看结束后隐藏播放按钮
  246. }
  247. })
  248. })
  249. this.player.on('play', () => {
  250. postMessage(
  251. {
  252. api: 'getDeviceStatus',
  253. content: {
  254. type: 'video'
  255. }
  256. },
  257. (res: any) => {
  258. // 判断是否在录屏中, 如果在录屏则不允许播放
  259. if (res.content.status == '1') {
  260. Toast('为了保证数据安全,请不要录屏')
  261. this.player.pause()
  262. }
  263. }
  264. )
  265. postMessage({
  266. api: 'limitScreenRecord',
  267. content: {
  268. type: 1
  269. }
  270. })
  271. this.onPlay && this.onPlay()
  272. })
  273. this.player.on('fullscreenchange', () => {
  274. if (this.player.isFullscreen()) {
  275. console.log('fullscreen')
  276. const i = document.createElement('i')
  277. i.id = 'fullscreen-back'
  278. i.className = 'van-icon van-icon-arrow-left video-back'
  279. i.addEventListener('click', () => {
  280. this.player.exitFullscreen()
  281. })
  282. // console.log(document.getElementsByClassName('video-js'))
  283. document.getElementsByClassName('video-js')[0].appendChild(i)
  284. } else {
  285. console.log('exitfullscreen')
  286. const i = document.getElementById('fullscreen-back')
  287. i && i.remove()
  288. }
  289. })
  290. }
  291. },
  292. // 操作功能
  293. domPlayVisibility(hide = true) {
  294. const controls = document.querySelector('.vjs-big-play-button')
  295. const controls2 = document.querySelector('.vjs-control-bar')
  296. if (hide) {
  297. controls?.setAttribute('style', 'display:none')
  298. controls2?.setAttribute('style', 'display:none')
  299. } else {
  300. controls?.removeAttribute('style')
  301. setTimeout(() => {
  302. controls2?.removeAttribute('style')
  303. }, 200)
  304. }
  305. },
  306. onClickPlay() {
  307. this.player.play()
  308. this.domPlayVisibility(false)
  309. this.showSeeStatus = false
  310. },
  311. onBuy() {
  312. if (this.isBuy) {
  313. this.onBuyEmit()
  314. return
  315. }
  316. this.$router.back()
  317. },
  318. onReplay() {
  319. this.player.currentTime(0)
  320. this.player.play()
  321. this.domPlayVisibility(false)
  322. this.trySeeOver = false
  323. this.showSeeStatus = false
  324. }
  325. },
  326. unmounted() {
  327. this.player?.pause()
  328. this.player?.src('')
  329. this.player?.dispose()
  330. },
  331. render() {
  332. return (
  333. <div
  334. class={[styles['video-container'], 'colVideo']}
  335. style={{
  336. height: this.height || '210px'
  337. }}
  338. >
  339. {/* <div ref="video" class={styles['video']} style={{ ...this.styleValue }}> */}
  340. {/* <Icon
  341. name="arrow-left"
  342. class={styles.videoBack}
  343. size="20"
  344. color="#fff"
  345. /> */}
  346. <video
  347. ref="video"
  348. class={styles['video']}
  349. src={this.src}
  350. id={this.videoID}
  351. playsinline={this.playsinline}
  352. poster={this.poster}
  353. preload={this.preload}
  354. style={{ ...this.styleValue }}
  355. ></video>
  356. {/* </div> */}
  357. {/* 加载视频使用 */}
  358. {this.loading && (
  359. <div
  360. class={styles.loadingVideo}
  361. style={{
  362. height: this.height || '210px'
  363. }}
  364. >
  365. <Loading
  366. size={36}
  367. color="#2dc7aa"
  368. vertical
  369. style={{ height: '100%', justifyContent: 'center' }}
  370. >
  371. 加载中...
  372. </Loading>
  373. </div>
  374. )}
  375. {/* 试看结束 */}
  376. {this.trySee && this.computedSeeStatus && !this.loading && (
  377. <div
  378. class={[styles.loadingVideo, styles.playOver]}
  379. style={{
  380. height: this.height || '210px'
  381. }}
  382. >
  383. {!this.trySeeOver ? (
  384. <>
  385. <Icon
  386. name={iconVideoPlay}
  387. size={50}
  388. onClick={this.onClickPlay}
  389. />
  390. <p class={styles.freeTxt}>
  391. 免费试看
  392. {/* {this.freeTitleStatus ? '试看' : '领取'} */}
  393. </p>
  394. {/* <p class={styles.freeRate}>每课时可试看{this.freeRate}%</p> */}
  395. </>
  396. ) : (
  397. <>
  398. {state.platformType === 'STUDENT' ? (
  399. <p class={styles.tips}>
  400. {this.freeTitleStatus
  401. ? '免费试看结束,购买完整课程后继续学习'
  402. : '试看结束,领取课程后继续学习'}
  403. </p>
  404. ) : (
  405. <p class={styles.tips}>
  406. 若需完整观看,请下载酷乐秀领取或购买
  407. </p>
  408. )}
  409. {state.platformType === 'STUDENT' && (
  410. <Button
  411. class={styles.btn}
  412. type="primary"
  413. round
  414. size="small"
  415. onClick={this.onBuy}
  416. >
  417. {state.platformType === 'STUDENT'
  418. ? this.freeTitleStatus
  419. ? '立即购买'
  420. : '免费领取'
  421. : '返回免费'}
  422. </Button>
  423. )}
  424. <div
  425. class={state.platformType !== 'STUDENT' && styles.replay}
  426. onClick={this.onReplay}
  427. >
  428. <Icon
  429. name="replay"
  430. style={{ marginRight: '5px' }}
  431. size={16}
  432. />
  433. 重播
  434. </div>
  435. </>
  436. )}
  437. </div>
  438. )}
  439. </div>
  440. )
  441. }
  442. })