video.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. import { defineComponent, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
  2. import styles from './video.module.less'
  3. // import poster from './images/video_bg.png'
  4. import { Button, Loading } from 'vant'
  5. import { browser } from '@/helpers/utils'
  6. import Plyr from 'plyr'
  7. import 'plyr/dist/plyr.css'
  8. import { useInterval, useIntervalFn } from '@vueuse/core'
  9. import { useRoute, useRouter } from 'vue-router'
  10. import request from '@/helpers/request'
  11. import qs from 'query-string'
  12. export default defineComponent({
  13. name: 'pre-register',
  14. setup() {
  15. const route = useRoute()
  16. const router = useRouter()
  17. const openId = sessionStorage.getItem('active-open-id')
  18. // 页面定时
  19. const pageTimer = useInterval(1000, { controls: true })
  20. pageTimer.pause()
  21. const forms = reactive({
  22. coverImg: '',
  23. introductionVideo: '',
  24. videoBrowsePoint: 0,
  25. saveId: route.query.saveId,
  26. orchestraId: route.query.id,
  27. openId: route.query.openId || openId,
  28. loading: false,
  29. player: null as any,
  30. playerSpeed: 1,
  31. intervalFnRef: null as any
  32. })
  33. // 播放视频总时长
  34. const videoIntervalRef = useInterval(1000, { controls: true })
  35. videoIntervalRef.pause()
  36. /**
  37. * 格式化视屏播放有效时间 - 合并区间
  38. * @param intervals [[], []]
  39. * @example [[4, 8],[0, 4],[10, 30]]
  40. * @returns [[0, 8], [10, 30]]
  41. */
  42. const formatEffectiveTime = (intervals: any[]) => {
  43. const res: any = []
  44. intervals.sort((a, b) => a[0] - b[0])
  45. let prev = intervals[0]
  46. for (let i = 1; i < intervals.length; i++) {
  47. const cur = intervals[i]
  48. if (prev[1] >= cur[0]) {
  49. // 有重合
  50. prev[1] = Math.max(cur[1], prev[1])
  51. } else {
  52. // 不重合,prev推入res数组
  53. res.push(prev)
  54. prev = cur // 更新 prev
  55. }
  56. }
  57. res.push(prev)
  58. return res
  59. }
  60. /**
  61. * 获取数据有效期
  62. * @param intervals [[], []]
  63. * @returns 0s
  64. */
  65. const formatTimer = (intervals: any[]) => {
  66. const afterIntervals = formatEffectiveTime(intervals)
  67. let time = 0
  68. afterIntervals.forEach((t: any) => {
  69. time += t[1] - t[0]
  70. })
  71. return time
  72. }
  73. /**
  74. * 视屏累计时长
  75. * 1、视屏开始播放时-开始计时
  76. * 2、视频暂停时暂停-停止计时
  77. * 3、视频加载时-停止计时
  78. * 4、视频倍数播放时,时间正常计时
  79. * 5、点击视频进度或拖动进度时,时间暂停
  80. */
  81. const _init = () => {
  82. const controls = [
  83. 'play-large',
  84. 'play',
  85. 'progress',
  86. 'captions',
  87. 'current-time',
  88. 'duration',
  89. 'settings',
  90. 'fullscreen'
  91. ]
  92. const params: any = {
  93. controls: controls,
  94. settings: ['speed'],
  95. speed: { selected: 1, options: [0.5, 1, 1.5, 2] },
  96. i18n: {
  97. speed: '速度',
  98. normal: '默认'
  99. },
  100. autoplay: false,
  101. invertTime: false
  102. }
  103. if (browser().iPhone) {
  104. params.fullscreen = {
  105. enabled: true,
  106. fallback: 'force',
  107. iosNative: true
  108. }
  109. }
  110. forms.player = new Plyr('#register-video', params)
  111. forms.player.on('loadedmetadata', () => {
  112. console.log('loadedmetadata')
  113. forms.loading = false
  114. forms.player.currentTime = forms.videoBrowsePoint
  115. })
  116. // 速度变化时
  117. forms.player.on('ratechange', () => {
  118. forms.playerSpeed =
  119. forms.playerSpeed < forms.player.speed ? forms.player.speed : forms.playerSpeed
  120. })
  121. forms.player.on('seeking', (val: any) => {
  122. // console.log('seeking')
  123. videoIntervalRef.isActive.value && videoIntervalRef.pause()
  124. })
  125. // // 拖动结束时
  126. forms.player.on('seeked', (val: any) => {
  127. // console.log('seeked')
  128. videoIntervalRef.isActive.value && videoIntervalRef.pause()
  129. })
  130. // 正在搜索中
  131. forms.player.on('waiting', () => {
  132. // console.log('waiting pause')
  133. videoIntervalRef.isActive.value && videoIntervalRef.pause()
  134. })
  135. // 如何视频在缓存不会触发
  136. forms.player.on('timeupdate', () => {
  137. // console.log('timeupdate', forms.player.currentTime)
  138. // 判断视频计时器是否暂停,如果暂停则恢复
  139. // 添加 「forms.player.playing」 是由会跳转到上次播放时间,会触发些方法
  140. if (
  141. !videoIntervalRef.isActive.value &&
  142. forms.player.currentTime > 0 &&
  143. forms.player.playing
  144. ) {
  145. // console.log('timeupdate play')
  146. videoIntervalRef.resume()
  147. }
  148. })
  149. // 视屏播放时暂停
  150. forms.player.on('ended', () => {
  151. forms.player.pause()
  152. })
  153. // 开始播放
  154. forms.player.on('play', () => {
  155. console.log('play')
  156. // 判断视频计时器是否暂停,如果暂停则恢复
  157. if (!videoIntervalRef.isActive.value) {
  158. videoIntervalRef.resume()
  159. }
  160. })
  161. // 暂停播放
  162. forms.player.on('pause', () => {
  163. console.log('pause')
  164. videoIntervalRef.pause()
  165. })
  166. forms.player.on('enterfullscreen', () => {
  167. console.log('fullscreen')
  168. const i = document.createElement('i')
  169. i.id = 'fullscreen-back'
  170. i.className = 'van-icon van-icon-arrow-left video-back'
  171. i.addEventListener('click', () => {
  172. forms.player.fullscreen.exit()
  173. })
  174. console.log(document.getElementsByClassName('plyr'))
  175. document.getElementsByClassName('plyr')[0].appendChild(i)
  176. })
  177. forms.player.on('exitfullscreen', () => {
  178. console.log('exitfullscreen')
  179. const i = document.getElementById('fullscreen-back')
  180. i && i.remove()
  181. })
  182. }
  183. // 保存零时时间
  184. const moreTime: any = ref([]) // 多个观看时间段
  185. let tempTime: any = [] // 临时存储时间
  186. const currentTimer = useInterval(1000, { controls: true })
  187. // 监听播放状态,
  188. watch(
  189. () => videoIntervalRef.isActive.value,
  190. (newVal: boolean) => {
  191. // console.log(newVal, 'videoIntervalRef.isActive.value in')
  192. // console.log('watch', forms.player.currentTime)
  193. // console.log('保留两个小数:', forms.player.currentTime.toFixed(2))
  194. // console.log('向下取整:', Math.floor(forms.player.currentTime))
  195. // console.log('向上取整:', Math.ceil(forms.player.currentTime))
  196. // console.log('四舍五入:', Math.round(forms.player.currentTime))
  197. if (newVal) {
  198. tempTime[0] = Math.floor(forms.player.currentTime)
  199. } else {
  200. tempTime[1] = Math.floor(forms.player.currentTime)
  201. }
  202. // console.log(forms.player.speed, 'speed')
  203. if (tempTime.length >= 2) {
  204. // console.log(tempTime, 'tempTime', moreTime.value)
  205. // 处理在短时间内的时间差 【视屏拖动,点击可能会导致时间差太大】
  206. const diffTime =
  207. tempTime[1] - tempTime[0] - currentTimer.counter.value * forms.playerSpeed > 2
  208. console.log(diffTime, 'diffTime', currentTimer.counter.value, 'value')
  209. // 结束时间,如果 大于开始时间则清除
  210. if (tempTime[1] >= tempTime[0] && !diffTime) moreTime.value.push(tempTime)
  211. tempTime = []
  212. currentTimer.counter.value = 0
  213. }
  214. // console.log('观看的时间', moreTime)
  215. }
  216. )
  217. // 更新时间
  218. const updateStat = async (pageBrowseTime = 10) => {
  219. try {
  220. const videoBrowseData = moreTime.value.length > 0 ? formatEffectiveTime(moreTime.value) : []
  221. const time = moreTime.value.length > 0 ? formatTimer(moreTime.value) : 0
  222. // const videoCountTime = videoIntervalRef?.counter.value
  223. const videoDuration = forms.player.duration
  224. // 判断如何视屏播放时间大于视屏播放有效时间则说明数据有问题,进行重置数据
  225. // if (time > videoCountTime && time < videoDuration) {
  226. // time = videoCountTime
  227. // }
  228. const rate = Math.floor((time / Math.floor(videoDuration)) * 100)
  229. await request.post('/api-student/open/studentBrowseRecord/updateStat', {
  230. data: {
  231. id: forms.saveId,
  232. pageBrowseTime, // 固定10秒
  233. videoBrowseData: JSON.stringify(videoBrowseData), // 视屏播放数据
  234. videoBrowseDataTime: time || 0, // 视屏观看百分比时长
  235. videoBrowsePercentage: rate || 0, // 视频观看百分比
  236. videoBrowseTime: videoIntervalRef?.counter.value, // 视频观看时长
  237. videoBrowsePoint: Math.floor(forms.player.currentTime || 0) // 视频最后观看点 - 向下取整
  238. }
  239. })
  240. } catch {
  241. //
  242. }
  243. }
  244. // 提交
  245. const onSubmit = async () => {
  246. try {
  247. // 暂停回调
  248. forms.intervalFnRef?.pause()
  249. currentTimer.pause()
  250. // 页面计时暂停
  251. pageTimer.pause()
  252. await updateStat()
  253. window.location.href =
  254. window.location.origin +
  255. window.location.pathname +
  256. '/project/preRegister.html?' +
  257. qs.stringify({
  258. orchestraId: forms.orchestraId,
  259. openId: forms.openId
  260. })
  261. // window.location.href =
  262. // window.location.origin +
  263. // '/project/preRegister.html?' +
  264. // qs.stringify({
  265. // orchestraId: forms.orchestraId,
  266. // openId: forms.openId
  267. // })
  268. } catch (e) {
  269. console.log(e, 'e')
  270. // 还原
  271. forms.intervalFnRef?.resume()
  272. pageTimer.resume()
  273. currentTimer.resume()
  274. }
  275. }
  276. onMounted(async () => {
  277. try {
  278. const { data } = await request.get('/api-student//open/studentBrowseRecord/query', {
  279. params: {
  280. openId: forms.openId,
  281. orchestraId: forms.orchestraId
  282. }
  283. })
  284. forms.videoBrowsePoint = data.videoBrowsePoint || 0
  285. if (forms.player) {
  286. forms.player.currentTime = data.videoBrowsePoint || 0
  287. }
  288. forms.introductionVideo = data.introductionVideo
  289. forms.coverImg = data.coverImg
  290. console.log(data)
  291. moreTime.value = data.videoBrowseData ? JSON.parse(data.videoBrowseData) : []
  292. _init()
  293. // 间隔多少时间同步数据
  294. forms.intervalFnRef = useIntervalFn(async () => {
  295. // 页面时间恢复
  296. pageTimer.counter.value = 0
  297. pageTimer.resume()
  298. await updateStat()
  299. videoIntervalRef.counter.value = 0
  300. }, 10000)
  301. } catch {
  302. //
  303. }
  304. })
  305. onUnmounted(() => {
  306. forms.intervalFnRef?.pause()
  307. currentTimer.pause()
  308. // 页面计时暂停
  309. pageTimer.pause()
  310. })
  311. // 判断是否有openId
  312. if (!forms.openId) {
  313. router.replace({
  314. path: '/pre-register-video',
  315. query: {
  316. id: forms.orchestraId
  317. }
  318. })
  319. }
  320. return () => (
  321. <div class={styles['pre-register-video']}>
  322. <div class={styles.videoContainer}>
  323. <div class={styles['video-content']}>
  324. <video
  325. id="register-video"
  326. class={styles['video']}
  327. src={forms.introductionVideo + '?time' + Date.now()}
  328. // src={
  329. // 'https://cloud-coach.ks3-cn-beijing.ksyuncs.com/1684981545808.mp4?time' + Date.now()
  330. // }
  331. playsinline={true}
  332. poster={forms.coverImg}
  333. preload="auto"
  334. ></video>
  335. {/* 加载视频使用 */}
  336. {forms.loading && (
  337. <div class={styles.loadingVideo}>
  338. <Loading
  339. size={36}
  340. color="#FF8057"
  341. vertical
  342. style={{ height: '100%', justifyContent: 'center' }}
  343. >
  344. 加载中...
  345. </Loading>
  346. </div>
  347. )}
  348. </div>
  349. </div>
  350. <div class={styles.messageContainer}>
  351. <div class={styles.messageContent}>
  352. <p>家长您好!</p>
  353. <p class={styles.c1}>
  354. 请家长们合理安排时间,<span>认真观看</span>家长会内容。在<span>详细了解</span>
  355. 所有要求后,有意向让孩子加入乐团的家长,请在<span>明晚20:00前</span>,为孩子完成
  356. <span>乐团报名</span>。
  357. </p>
  358. <p class={styles.c1}>
  359. 下周,专业老师将针对意向入团学员进行身体条件确认。谢谢各位的支持!
  360. </p>
  361. <p class={styles.bottom}>
  362. 注:乐团于下学期正式开始训练,训练时间下学期开学前另行通知,训练时间会与学校其他社团错开,家长无需担心时间冲突问题。
  363. </p>
  364. </div>
  365. <Button class={styles.submitBtn} onClick={onSubmit}></Button>
  366. </div>
  367. </div>
  368. )
  369. }
  370. })