videoBox.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. <template>
  2. <view class="video-wrap" @click="handleClickWrap">
  3. <video
  4. autoplay
  5. object-fit="contain"
  6. show-mute-btn
  7. enable-play-gesture
  8. :poster="videoInfo.cover_img_url"
  9. :src="videoInfo.video_url"
  10. :id="videoInfo.id"
  11. @ended="handleVideoEnd"
  12. @play="handleVideoPlay"
  13. @pause="handleVideoPause"
  14. @timeupdate="handleTimeUpdate"
  15. @fullscreenchange="handleFullscreenchange"
  16. @controlstoggle="handleControlstoggle"
  17. @click.stop="handleClickVideo"
  18. v-if="videoInfo.id==curVideoId"
  19. >
  20. <!-- 弹幕滚动模块 -->
  21. <view class="danmu-scroll-box" v-show="!closeDM">
  22. <view
  23. :class="[
  24. 'danmu-item',
  25. play?'animat-run':'animat-pause',
  26. item.user_id==selfUserid?'danmu-item-self':''
  27. ]"
  28. v-for="item in danmuList"
  29. :key="item.id"
  30. :style="{color:item.color,top:item.top,animationDuration:isFullScreen?item.speed+7+'s':item.speed+'s'}"
  31. >{{item.content}}</view>
  32. </view>
  33. <view class="video-inner-right-box" v-if="isShowControls">
  34. <!-- 切换音频播放按钮 -->
  35. <view :class="['change-music-icon',isFullScreen&&'isFullScreen']" @click.stop="handleChangeMusic"></view>
  36. <!-- 弹幕控制按钮 -->
  37. <view :class="['video-danmu-control-box',isFullScreen&&'isFullScreen']">
  38. <view class="show-btn" v-if="!closeDM" @click.stop="closeDM=true"></view>
  39. <view class="close-btn" v-else @click.stop="closeDM=false"></view>
  40. <!-- <view class="send-btn" v-if="!closeDM" @click.stop="showInput=true">发弹幕</view> -->
  41. </view>
  42. <!-- 倍速控制按钮 -->
  43. <view class="video-speed-btn" @click.stop="showSpeedOpt=true">倍速</view>
  44. </view>
  45. <!-- 倍速选项模块 -->
  46. <view class="speed-opt-box" v-if="showSpeedOpt">
  47. <view
  48. class="item"
  49. :style="{color:item==curSpeed?'#F3A52F':''}"
  50. v-for="item in speedOpts"
  51. :key="item"
  52. @click.stop="handleVideoSpeedChange(item)"
  53. >{{item}}X</view>
  54. </view>
  55. </video>
  56. <image @click="handelClickPlay" v-else class="poster" :src="videoInfo.cover_img_url" mode="aspectFill" lazy-load/>
  57. <!-- 外面弹幕按钮 -->
  58. <view class="danmu-btn-box" v-if="showDanmu">
  59. <view class="big-box" v-if="!closeDM">
  60. <view class="left" @click.stop="showInput=true"></view>
  61. <view class="right" @click.stop="closeDM=true"></view>
  62. </view>
  63. <view class="small-box" v-else @click.stop="handleShowDM"></view>
  64. </view>
  65. <!-- 弹幕输入弹窗 -->
  66. <view class="flex danmu-input-box" :style="{bottom:keyboardheight+'px',paddingLeft:isFullScreen?safeAreaTop+17+'px':'34rpx'}" v-if="showInput">
  67. <view class="flex input-box">
  68. <input
  69. type="text"
  70. v-model="danmuText"
  71. placeholder="发个友善的弹幕见证当下~"
  72. cursor-spacing="20"
  73. maxlength="50"
  74. focus
  75. confirm-type="send"
  76. :adjust-position="false"
  77. @confirm="handleSendDanmu"
  78. @focus="handleDanmuInputFocus"
  79. @blur="handleDanmuInputBlur"
  80. />
  81. <text>{{danmuText.length}}/50</text>
  82. </view>
  83. <view class="btn" @click="handleSendDanmu">发送</view>
  84. </view>
  85. </view>
  86. </template>
  87. <script>
  88. import {apiVideoDanmuSend} from '@/api/video'
  89. import { debounce } from '@/utils/common.js';
  90. export default {
  91. props:{
  92. showDanmu:{
  93. type:Boolean,
  94. default:false
  95. },//是否显示弹幕
  96. videoInfo:{},//{source:2-视频社区\3-路演视频,id:视频社区id\路演视频id,...其他数据}
  97. curVideoId:0,//当前正在播放的id
  98. },
  99. watch: {
  100. curVideoId(){
  101. this.curSpeed='1.0'
  102. if(this.videoInfo.id==this.curVideoId){//显示弹幕
  103. this.closeDM=false
  104. }else{
  105. this.closeDM=true
  106. this.curVideoIns=null
  107. this.play=false
  108. this.danmuList=[]
  109. this.temdanmuList.forEach(item=>{
  110. item.done=false
  111. })
  112. }
  113. },
  114. showInput(n){
  115. //写弹幕时暂停视频
  116. if(n){
  117. this.curVideoIns.pause()
  118. }else{
  119. this.curVideoIns.play()
  120. }
  121. },
  122. closeDM(){
  123. if(this.closeDM){
  124. this.danmuList=[]//如果关闭了弹幕显示 则清空一次 弹幕列表 防止 切换回来又会重新播放一次已有的弹幕
  125. }
  126. }
  127. },
  128. computed:{
  129. selfUserid(){
  130. return this.$store.state.user.userInfo.user_id;
  131. }
  132. },
  133. data() {
  134. return {
  135. curVideoIns:null,
  136. play:false,//视频播放状态
  137. curVideoTime:0,
  138. showInput:false,//显示悬浮输入弹幕弹窗
  139. closeDM:true,//是否关闭弹幕
  140. danmuText:'',
  141. showSpeedOpt:false,
  142. speedOpts:['0.5','0.8','1.0','1.25','1.5','2.0'],
  143. curSpeed:'1.0',
  144. keyboardheight:0,//键盘高度
  145. isBlur:false,//是否弹幕输入框失焦了
  146. isFullScreen:false,//是否为全屏
  147. isShowControls:false,//是否显示视频的控制栏
  148. safeAreaTop:0,
  149. temdanmuList:this.videoInfo.bullet_chat_list||[],
  150. danmuList:[],
  151. }
  152. },
  153. created(){
  154. uni.getSystemInfo({
  155. success:(res)=>{
  156. this.safeAreaTop=res.safeArea.top
  157. }
  158. })
  159. },
  160. methods: {
  161. //点击最外层盒子
  162. handleClickWrap(){
  163. this.showSpeedOpt=false
  164. },
  165. handleClickVideo(){
  166. this.showSpeedOpt=false
  167. },
  168. // 点击播放
  169. handelClickPlay(){
  170. this.$emit('videoPlay', this.videoInfo)
  171. setTimeout(() => {
  172. this.curVideoIns=uni.createVideoContext(this.curVideoId.toString(),this)//由于是在自定义组件内 所有this不可少
  173. }, 300);
  174. },
  175. handleVideoPlay(){
  176. this.play=true
  177. },
  178. handleVideoEnd(){
  179. // 此处因为如果不调用退出全屏方法 安卓和ios页面均会表现异常,安卓横屏不恢复竖屏,ios底部tabbar渲染异常
  180. this.curVideoIns.exitFullScreen()
  181. this.curVideoIns=null
  182. this.play=false
  183. this.danmuList=[]
  184. this.temdanmuList.forEach(item=>{
  185. item.done=false
  186. })
  187. this.$emit('ended')
  188. },
  189. handleVideoPause(){
  190. this.play=false
  191. this.$emit('pause')
  192. },
  193. handleTimeUpdate(e){
  194. this.addDanmu(e.detail.currentTime)
  195. this.curVideoTime=e.detail.currentTime
  196. this.$emit('timeupdate',e)
  197. },
  198. handleFullscreenchange(e){
  199. this.isFullScreen=e.detail.fullScreen
  200. },
  201. handleControlstoggle(e){
  202. this.isShowControls=e.detail.show
  203. },
  204. handleDanmuInputFocus(e){
  205. console.log(e.detail.height);
  206. this.keyboardheight=e.detail.height
  207. },
  208. handleDanmuInputBlur:debounce(function(){
  209. if(this.showInput){
  210. this.showInput=false
  211. }
  212. },60),
  213. //切换为背景音频播放
  214. handleChangeMusic(){
  215. console.log('切换背景音频播放');
  216. this.curVideoIns.requestBackgroundPlayback()
  217. },
  218. // 倍速切换
  219. handleVideoSpeedChange(item){
  220. const num=Number(item)
  221. this.curVideoIns.playbackRate(num)
  222. this.curSpeed=item
  223. this.showSpeedOpt=false
  224. },
  225. //点击视频区域外面的 显示弹幕按钮 进行判断如果当前视频没有播放 则不改变状态
  226. handleShowDM(){
  227. if(this.videoInfo.id==this.curVideoId){
  228. this.closeDM=false
  229. }
  230. },
  231. //发送弹幕
  232. handleSendDanmu(){
  233. if(!this.danmuText) return
  234. apiVideoDanmuSend({
  235. content:this.danmuText,
  236. seconds:parseInt(this.curVideoTime),
  237. primary_id:this.videoInfo.id,
  238. source:this.videoInfo.source
  239. }).then(res=>{
  240. this.danmuText=''
  241. if(res.code===200){
  242. this.temdanmuList.push({...res.data,seconds:Number(res.data.seconds)+3})
  243. }
  244. })
  245. },
  246. //添加弹幕到视频上去
  247. addDanmu(ctime){
  248. this.temdanmuList.forEach(item => {
  249. if(item.seconds>ctime-1&&item.seconds<ctime+1){// 前后误差一秒
  250. if(!item.done){
  251. item.done=true
  252. this.danmuList.push({
  253. ...item,
  254. top:this.getTopPosition(),
  255. speed:Math.floor(Math.random()*(16-8+1))+8//8~16 之间的随机数
  256. })
  257. }
  258. }else{
  259. // 如果播放过了 手贱又把进度条拖回去了 则重置done
  260. if(ctime-1<item.seconds){
  261. item.done=false
  262. }
  263. if(ctime+1>item.seconds){
  264. item.done=true
  265. }
  266. }
  267. });
  268. },
  269. //设置弹幕位置
  270. getTopPosition(){
  271. const length=this.danmuList.length
  272. let num=0
  273. if(length%3===1){
  274. num=10
  275. }else if(length%3===2){
  276. num=30
  277. }else{
  278. num=50
  279. }
  280. return num+'px'
  281. }
  282. },
  283. }
  284. </script>
  285. <style lang="scss" scoped>
  286. .video-wrap{
  287. width: 100%;
  288. height: 100%;
  289. position: relative;
  290. video,.poster{
  291. width: 100%;
  292. height: 100%;
  293. border-radius: 20rpx;
  294. position: relative;
  295. }
  296. .poster{
  297. position: relative;
  298. &::after{
  299. content:'';
  300. display: block;
  301. position: absolute;
  302. width: 120rpx;
  303. height: 120rpx;
  304. top: 50%;
  305. left: 50%;
  306. transform: translate(-50%,-50%);
  307. background-image: url('@/static/video-play-btn.png');
  308. background-size: cover;
  309. }
  310. }
  311. .danmu-btn-box{
  312. position: absolute;
  313. bottom: -70rpx;
  314. right: 6rpx;
  315. .big-box{
  316. width: 248rpx;
  317. height: 50rpx;
  318. background-image: url('@/static/danmu-show-btn.png');
  319. background-size: cover;
  320. display: flex;
  321. .left{
  322. width: 67%;
  323. height: 100%;
  324. flex-shrink: 0;
  325. }
  326. .right{
  327. flex: 1;
  328. height: 100%;
  329. }
  330. }
  331. .small-box{
  332. width: 80rpx;
  333. height: 50rpx;
  334. background-image: url('@/static/danmu-close-btn.png');
  335. background-size: cover;
  336. }
  337. }
  338. .danmu-input-box{
  339. position: fixed;
  340. left: 0;
  341. right: 0;
  342. bottom: 0;
  343. z-index: 999999999;
  344. background-color: #ffffff;
  345. padding: 10rpx 34rpx;
  346. border-top: 1px solid #E5E5E5;
  347. align-content: center;
  348. .input-box{
  349. flex:1;
  350. border-radius: 40rpx;
  351. padding: 6rpx 10rpx;
  352. background: #EFEFEF;
  353. border: 2rpx solid #E5E5E5;
  354. align-items: center;
  355. font-size: 12px;
  356. input{
  357. flex:1;
  358. }
  359. text{
  360. color: #999;
  361. }
  362. }
  363. .btn{
  364. width: 100rpx;
  365. flex-shrink: 0;
  366. color: #F3A52F;
  367. font-size: 12px;
  368. display: flex;
  369. align-items: center;
  370. justify-content: center;
  371. }
  372. }
  373. .video-inner-right-box{
  374. position: absolute;
  375. bottom: 30%;
  376. right: 5%;
  377. display: flex;
  378. flex-direction: column;
  379. align-items: center;
  380. }
  381. .change-music-icon{
  382. width: 40rpx;
  383. height: 40rpx;
  384. background-image: url('@/static/headphones-icon.png');
  385. background-size: cover;
  386. margin-bottom: 20rpx;
  387. &.isFullScreen{
  388. margin-bottom: 30rpx;
  389. }
  390. }
  391. .video-speed-btn{
  392. width: 80rpx;
  393. height: 44rpx;
  394. display: flex;
  395. align-items: center;
  396. justify-content: center;
  397. background: rgba(0, 0, 0, 0.4);
  398. border-radius: 22rpx;
  399. color: #fff;
  400. font-size: 12px;
  401. }
  402. .speed-opt-box{
  403. position: absolute;
  404. right: 0;
  405. top: 0;
  406. bottom: 0;
  407. width: 20%;
  408. background: rgba(0, 0, 0, 0.8);
  409. display: flex;
  410. flex-direction: column;
  411. justify-content: space-around;
  412. padding-top: 55rpx;
  413. .item{
  414. color: #fff;
  415. font-size: 26rpx;
  416. flex: 1;
  417. width: 100%;
  418. text-align: center;
  419. }
  420. }
  421. .video-danmu-control-box{
  422. margin-bottom: 10rpx;
  423. &.isFullScreen{
  424. margin-bottom: 30rpx;
  425. .send-btn{
  426. margin-top: 30rpx;
  427. }
  428. }
  429. view{
  430. margin-left: auto;
  431. margin-right: auto;
  432. }
  433. .show-btn{
  434. width: 80rpx;
  435. height: 50rpx;
  436. background-image: url('@/static/danmu-show-btn-2.png');
  437. background-size: cover;
  438. }
  439. .close-btn{
  440. width: 80rpx;
  441. height: 50rpx;
  442. background-image: url('@/static/danmu-close-btn-2.png');
  443. background-size: cover;
  444. }
  445. .send-btn{
  446. width: 100rpx;
  447. height: 44rpx;
  448. display: flex;
  449. align-items: center;
  450. justify-content: center;
  451. background: rgba(0, 0, 0, 0.4);
  452. border-radius: 22rpx;
  453. color: #fff;
  454. font-size: 12px;
  455. margin-top: 20rpx;
  456. }
  457. }
  458. .danmu-scroll-box{
  459. .danmu-item{
  460. color: #fff;
  461. animation: move 6s linear;
  462. position: absolute;
  463. top: 0;
  464. display: block;
  465. left: 150%;
  466. position: absolute;
  467. font-size: 12px;
  468. height: 18px;
  469. white-space: nowrap;
  470. background: rgba(48, 48, 48, 0.5);
  471. padding-left: 10rpx;
  472. padding-right: 10rpx;
  473. border-radius: 18px;
  474. padding-top: 9px;
  475. }
  476. .animat-pause{
  477. animation-play-state: paused;
  478. }
  479. .animat-run{
  480. animation-play-state: running;
  481. }
  482. .danmu-item-self{
  483. color: #F9AC3A !important;
  484. }
  485. @keyframes move {
  486. 0%{
  487. left: 150%;
  488. }
  489. 100%{
  490. left: -200%;
  491. }
  492. }
  493. }
  494. }
  495. </style>