index.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  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. import request from '@/helpers/request'
  13. export default defineComponent({
  14. name: 'col-video',
  15. props: {
  16. trySee: {
  17. // 是否试看
  18. type: Boolean,
  19. default: false
  20. },
  21. // 收费方式
  22. payType: {
  23. type: String,
  24. default: ''
  25. },
  26. freeTitleStatus: {
  27. type: Boolean,
  28. default: true
  29. },
  30. // 试看比例
  31. freeRate: {
  32. type: Number,
  33. default: 100
  34. },
  35. setting: {
  36. type: Object,
  37. default: () => {}
  38. },
  39. controls: Boolean,
  40. height: String,
  41. src: {
  42. type: String,
  43. default: ''
  44. },
  45. poster: {
  46. type: String,
  47. default: ''
  48. },
  49. styleValue: {
  50. type: Object,
  51. default: () => ({})
  52. },
  53. preload: {
  54. type: String as PropType<'auto' | 'metadata' | 'none'>,
  55. default: 'auto'
  56. },
  57. currentTime: {
  58. type: Boolean,
  59. default: true
  60. },
  61. playsinline: {
  62. type: Boolean,
  63. default: true
  64. },
  65. onPlay: {
  66. type: Function,
  67. default: () => {}
  68. },
  69. isBuy: {
  70. // 是否把购买方法在外部调用
  71. type: Boolean,
  72. default: false
  73. },
  74. onBuyEmit: {
  75. type: Function,
  76. default: () => {}
  77. }
  78. },
  79. data() {
  80. return {
  81. disableScreenRecordingFlag: '0', // disable recording
  82. videoID: 'video' + Date.now() + Math.floor(Math.random() * 100),
  83. player: null as any,
  84. // playTime: 0,
  85. loading: true, // 首次进入加载中
  86. trySeeOver: false, // 试看是否结束
  87. showSeeStatus: true // 是否显示试看状态
  88. }
  89. },
  90. watch: {
  91. src() {
  92. console.log('src, change')
  93. this._init()
  94. }
  95. },
  96. async mounted() {
  97. await this.sysParamConfig()
  98. const Button = TCPlayer.getComponent('Button')
  99. const BigPlayButton = TCPlayer.getComponent('BigPlayButton')
  100. BigPlayButton.prototype.createEl = function () {
  101. const el = Button.prototype.createEl.call(this)
  102. const _html =
  103. '<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>'
  104. el.appendChild(
  105. TCPlayer.dom.createEl('div', {
  106. className: 'vjs-button-icon',
  107. innerHTML: _html
  108. })
  109. )
  110. return el
  111. }
  112. this.player = TCPlayer(this.videoID, {
  113. appID: '',
  114. controls: true
  115. }) // player-container-id 为播放器容器 ID,必须与 html 中一致
  116. this._init()
  117. listenerMessage('setVideoPlayer', result => {
  118. const content = result?.content
  119. if (content.status === 'pause') {
  120. this.player.pause()
  121. }
  122. })
  123. },
  124. beforeUnmount() {
  125. postMessage({
  126. api: 'limitScreenRecord',
  127. content: {
  128. type: 0
  129. }
  130. })
  131. },
  132. computed: {
  133. computedSeeStatus() {
  134. // console.log(
  135. // this.showSeeStatus,
  136. // this.trySee,
  137. // this.trySeeOver,
  138. // 'this.showSeeStatus, this.trySee'
  139. // )
  140. return this.showSeeStatus && this.trySee
  141. },
  142. playTime() {
  143. // 允许播放时间
  144. const player = this.player
  145. const playTime = (player.duration() * this.freeRate) / 100
  146. return playTime || 0
  147. }
  148. },
  149. methods: {
  150. // 获取支付渠道
  151. async sysParamConfig() {
  152. try {
  153. const apiSuffix =
  154. state.platformType === 'STUDENT' ? '/api-student' : '/api-teacher'
  155. const res = await request.get(
  156. apiSuffix + '/sysConfig/queryByParamName',
  157. {
  158. params: {
  159. paramName: 'disable_screen_recording_flag'
  160. }
  161. }
  162. )
  163. this.disableScreenRecordingFlag = res.data.paramValue || ''
  164. } catch {
  165. //
  166. }
  167. },
  168. _init() {
  169. // const controls = [
  170. // 'play-large',
  171. // 'play',
  172. // 'progress',
  173. // 'captions',
  174. // 'fullscreen'
  175. // ]
  176. // if (this.currentTime) {
  177. // controls.push('current-time')
  178. // }
  179. // const params: any = {
  180. // controls: controls,
  181. // ...this.setting,
  182. // invertTime: false
  183. // }
  184. // if (browser().iPhone) {
  185. // params.fullscreen = {
  186. // enabled: true,
  187. // fallback: 'force',
  188. // iosNative: true
  189. // }
  190. // }
  191. // this.player = new Plyr((this as any).$refs.video, params)
  192. // this.player.elements.container
  193. // ? (this.player.elements.container.style.height = this.height || '210px')
  194. // : null
  195. // if (this.preload === 'none') {
  196. // this.loading = false
  197. // }
  198. // this.player.on('loadedmetadata', () => {
  199. // this.loading = false
  200. // if (this.trySee) {
  201. // this.domPlayVisibility()
  202. // } else {
  203. // this.domPlayVisibility(false)
  204. // }
  205. // // 监听播放事件
  206. // const _this = this
  207. // this.player.on('timeupdate', () => {
  208. // const players = _this.player
  209. // if (players.currentTime >= this.playTime && _this.trySee) {
  210. // players.pause()
  211. // _this.trySeeOver = true
  212. // _this.showSeeStatus = true
  213. // _this.domPlayVisibility() // 试看结束后隐藏播放按钮
  214. // }
  215. // })
  216. // })
  217. // this.player.on('play', () => {
  218. // postMessage(
  219. // {
  220. // api: 'getDeviceStatus',
  221. // content: {
  222. // type: 'video'
  223. // }
  224. // },
  225. // (res: any) => {
  226. // // 判断是否在录屏中, 如果在录屏则不允许播放
  227. // if (res.content.status == '1') {
  228. // Toast('为了保证数据安全,请不要录屏')
  229. // this.player.pause()
  230. // }
  231. // }
  232. // )
  233. // postMessage({
  234. // api: 'limitScreenRecord',
  235. // content: {
  236. // type: 1
  237. // }
  238. // })
  239. // this.onPlay && this.onPlay()
  240. // })
  241. // this.player.on('enterfullscreen', () => {
  242. // console.log('fullscreen')
  243. // const i = document.createElement('i')
  244. // i.id = 'fullscreen-back'
  245. // i.className = 'van-icon van-icon-arrow-left video-back'
  246. // i.addEventListener('click', () => {
  247. // this.player.fullscreen.exit()
  248. // })
  249. // console.log(document.getElementsByClassName('plyr'))
  250. // document.getElementsByClassName('plyr')[0].appendChild(i)
  251. // })
  252. // this.player.on('exitfullscreen', () => {
  253. // console.log('exitfullscreen')
  254. // const i = document.getElementById('fullscreen-back')
  255. // i && i.remove()
  256. // })
  257. if (this.player && this.src) {
  258. this.player.src(this.src) // url 播放地址
  259. this.player.poster(this.poster || '')
  260. if (this.preload === 'none') {
  261. this.loading = false
  262. }
  263. this.player.on('loadstart', () => {
  264. this.loading = false
  265. if (this.trySee) {
  266. this.domPlayVisibility()
  267. } else {
  268. this.domPlayVisibility(false)
  269. }
  270. // 监听播放事件
  271. this.player.on('timeupdate', () => {
  272. const players = this.player
  273. if (players.currentTime() >= this.playTime && this.trySee) {
  274. players.pause()
  275. this.trySeeOver = true
  276. this.showSeeStatus = true
  277. this.domPlayVisibility() // 试看结束后隐藏播放按钮
  278. }
  279. })
  280. })
  281. this.player.on('play', () => {
  282. if (this.disableScreenRecordingFlag === '1') {
  283. postMessage(
  284. {
  285. api: 'getDeviceStatus',
  286. content: {
  287. type: 'video'
  288. }
  289. },
  290. (res: any) => {
  291. // 判断是否在录屏中, 如果在录屏则不允许播放
  292. if (res.content.status == '1') {
  293. Toast('为了保证数据安全,请不要录屏')
  294. this.player.pause()
  295. }
  296. }
  297. )
  298. postMessage({
  299. api: 'limitScreenRecord',
  300. content: {
  301. type: 1
  302. }
  303. })
  304. }
  305. this.onPlay && this.onPlay()
  306. })
  307. this.player.on('fullscreenchange', () => {
  308. if (this.player.isFullscreen()) {
  309. console.log('fullscreen')
  310. const i = document.createElement('i')
  311. i.id = 'fullscreen-back'
  312. i.className = 'van-icon van-icon-arrow-left video-back'
  313. i.addEventListener('click', () => {
  314. this.player.exitFullscreen()
  315. })
  316. // console.log(document.getElementsByClassName('video-js'))
  317. document.getElementsByClassName('video-js')[0].appendChild(i)
  318. } else {
  319. console.log('exitfullscreen')
  320. const i = document.getElementById('fullscreen-back')
  321. i && i.remove()
  322. }
  323. })
  324. }
  325. },
  326. // 操作功能
  327. domPlayVisibility(hide = true) {
  328. const controls = document.querySelector('.vjs-big-play-button')
  329. const controls2 = document.querySelector('.vjs-control-bar')
  330. if (hide) {
  331. controls?.setAttribute('style', 'display:none')
  332. controls2?.setAttribute('style', 'display:none')
  333. } else {
  334. controls?.removeAttribute('style')
  335. setTimeout(() => {
  336. controls2?.removeAttribute('style')
  337. }, 200)
  338. }
  339. },
  340. onClickPlay() {
  341. this.player.play()
  342. this.domPlayVisibility(false)
  343. this.showSeeStatus = false
  344. },
  345. onBuy() {
  346. if (this.isBuy) {
  347. this.onBuyEmit()
  348. return
  349. }
  350. this.$router.back()
  351. },
  352. onReplay() {
  353. this.player.currentTime(0)
  354. this.player.play()
  355. this.domPlayVisibility(false)
  356. this.trySeeOver = false
  357. this.showSeeStatus = false
  358. }
  359. },
  360. unmounted() {
  361. this.player?.pause()
  362. this.player?.src('')
  363. this.player?.dispose()
  364. },
  365. render() {
  366. return (
  367. <div
  368. class={[styles['video-container'], 'colVideo']}
  369. style={{
  370. height: this.height || '210px'
  371. }}
  372. >
  373. {/* <div ref="video" class={styles['video']} style={{ ...this.styleValue }}> */}
  374. {/* <Icon
  375. name="arrow-left"
  376. class={styles.videoBack}
  377. size="20"
  378. color="#fff"
  379. /> */}
  380. <video
  381. ref="video"
  382. class={styles['video']}
  383. // src={this.src}
  384. id={this.videoID}
  385. playsinline={this.playsinline}
  386. poster={this.poster}
  387. preload={this.preload}
  388. style={{ ...this.styleValue }}
  389. ></video>
  390. {/* </div> */}
  391. {/* 加载视频使用 */}
  392. {this.loading && (
  393. <div
  394. class={styles.loadingVideo}
  395. style={{
  396. height: this.height || '210px'
  397. }}
  398. >
  399. <Loading
  400. size={36}
  401. color="#2dc7aa"
  402. vertical
  403. style={{ height: '100%', justifyContent: 'center' }}
  404. >
  405. 加载中...
  406. </Loading>
  407. </div>
  408. )}
  409. {/* 试看结束 */}
  410. {this.trySee && this.computedSeeStatus && !this.loading && (
  411. <div
  412. class={[styles.loadingVideo, styles.playOver]}
  413. style={{
  414. height: this.height || '210px'
  415. }}
  416. >
  417. {!this.trySeeOver ? (
  418. <>
  419. <Icon
  420. name={iconVideoPlay}
  421. size={50}
  422. onClick={this.onClickPlay}
  423. />
  424. <p class={styles.freeTxt}>
  425. 免费试看
  426. {/* {this.freeTitleStatus ? '试看' : '领取'} */}
  427. </p>
  428. {/* <p class={styles.freeRate}>每课时可试看{this.freeRate}%</p> */}
  429. </>
  430. ) : (
  431. <>
  432. {state.platformType === 'STUDENT' ? (
  433. <p class={styles.tips}>
  434. {this.freeTitleStatus
  435. ? '免费试看结束,购买完整课程后继续学习'
  436. : this.payType === 'VIP'
  437. ? '免费试看结束,开通会员后继续学习'
  438. : '试看结束,领取课程后继续学习'}
  439. </p>
  440. ) : (
  441. <p class={styles.tips}>
  442. {/* 若需完整观看,请下载酷乐秀领取或购买 */}
  443. 试看结束
  444. </p>
  445. )}
  446. {state.platformType === 'STUDENT' && (
  447. <Button
  448. class={styles.btn}
  449. type="primary"
  450. round
  451. size="small"
  452. onClick={this.onBuy}
  453. >
  454. {state.platformType === 'STUDENT'
  455. ? this.freeTitleStatus
  456. ? '立即购买'
  457. : this.payType === 'VIP'
  458. ? '开通会员'
  459. : '免费领取'
  460. : '返回免费'}
  461. </Button>
  462. )}
  463. <div
  464. class={state.platformType !== 'STUDENT' && styles.replay}
  465. onClick={this.onReplay}
  466. >
  467. <Icon
  468. name="replay"
  469. style={{ marginRight: '5px' }}
  470. size={16}
  471. />
  472. 重播
  473. </div>
  474. </>
  475. )}
  476. </div>
  477. )}
  478. </div>
  479. )
  480. }
  481. })