2022年5月29日从svn移到git

This commit is contained in:
daihh
2022-05-29 18:56:34 +08:00
commit b050613020
488 changed files with 68444 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
# vue弹幕视频播放器
简体中文 / [English](https://github.com/yleencc/vue-barrage-videoplayer/blob/master/README_EN.md)
## 简介
> 一个基于Vue的弹幕视频播放器组件。适用于`PC版网页`、`静态资源视频`
## 功能支持
> 主要功能:播放、音量控制、倍速、进度条控制、全屏、弹幕(部分)
>
> 键盘控制F全屏↑↓键调整音量←→调整进度Space暂停/继续
### 弹幕支持情况
> 格式只支持B站xml弹幕格式详细格式情况參考[这篇文章](https://blog.csdn.net/Enderman_xiaohei/article/details/86659064)
>
> 操作:目前只能显示弹幕不能做其他操作
## 预览示例
- [点击跳转预览](https://yleen.cc/files/works/barrage-video-player/)
---
## 计划表
- [X] 多播放器共存
- [X] 弹幕(开发了一小部分)
- [ ] 网页全屏
- [ ] Vue升级到3.x版本
- [ ] 右键查看视频数据
- [ ] 多语言支持
- [ ] 推流视频websocket
- [ ] 适配好移动端
## 更新历史
- 0.1.1 增加了对同时使用多个播放器的支持,以及封面图
- 0.1.2 优化了遗留的部分烂代码
- 0.2.0 支持显示滑动弹幕,预留导入外部弹幕文件功能
## 使用方式与示例
可参考[preview-videoplayer.vue](https://github.com/yleencc/vue-barrage-videoplayer/blob/master/src/views/preview-videoplayer.vue) 文件
1.`src/components`下的四个文件复制到你的项目里,确保这四个文件保持在同一目录;
2.`src/assets`文件夹复制到项目`src/`里面,确保`/src/assets/images/loading.svg`存在(该文件为缓冲时的加载图标)
3. 在页面中引用`barrage-videoplayer.vue`
- 注:项目需要引入`axios`
``` vue
<template>
<barrageVideoplayer
:src="video_src"
:cover="cover"
:biBarrageXml="biBarrageXml"
width="100%"
height="100%"
></barrageVideoplayer>
</template>
<script>
import barrageVideoplayer from "../components/barrage-videoplayer.vue";
export default {
components: {barrageVideoplayer},
data() {
return {
video_src: "",
cover: "",
biBarrageXml: "",
};
},
}
```
## 传入参数
`barrage-videoplayer.vue`
| 字段 | 是否可选 | 类型 | 默认值 | 备注 |
|---|---|---|---|---|
| src | `必选` | String | null | 视频链接 |
| cover | `可选` | String | null | 封面图的链接 |
| primaryColor | `可选` | String | "cornflowerblue" | 主题色可使用css属性值颜色名称、#十六进制、rgb、rgba、HSL、HSLA |
| width | `可选` | String | 100% | 视频宽度对应css样式 |
| height | `可选` | String | 100% | 视频高度对应css样式 |
| speed_list | `可选` | array | ["2.0", "1.5", "1.25", "1.0", "0.75", "0.5"] | 倍速选择的列表 |
| biBarrageXml | `可选` | String | null | 弹幕链接使用的是B站XML风格需处理跨域问题 |
### bug反馈
- 提[issue](https://github.com/yleencc/vue-barrage-videoplayer/issues)
- 本人首页的联系方式,邮箱优先
---
### Project setup
```
npm install
```
#### Compiles and hot-reloads for development
```
npm run serve
```
#### Compiles and minifies for production
```
npm run build
```
#### Lints and fixes files
```
npm run lint
```
#### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: none; display: block; shape-rendering: auto;" width="150px" height="150px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<circle cx="84" cy="50" r="10" fill="#d2d2cb">
<animate attributeName="r" repeatCount="indefinite" dur="0.41666666666666663s" calcMode="spline" keyTimes="0;1" values="10;0" keySplines="0 0.5 0.5 1" begin="0s"></animate>
<animate attributeName="fill" repeatCount="indefinite" dur="1.6666666666666665s" calcMode="discrete" keyTimes="0;0.25;0.5;0.75;1" values="#d2d2cb;#dae8e5;#83a79d;#4d695d;#d2d2cb" begin="0s"></animate>
</circle><circle cx="16" cy="50" r="10" fill="#d2d2cb">
<animate attributeName="r" repeatCount="indefinite" dur="1.6666666666666665s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;10;10;10" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="0s"></animate>
<animate attributeName="cx" repeatCount="indefinite" dur="1.6666666666666665s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="0s"></animate>
</circle><circle cx="50" cy="50" r="10" fill="#4d695d">
<animate attributeName="r" repeatCount="indefinite" dur="1.6666666666666665s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;10;10;10" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-0.41666666666666663s"></animate>
<animate attributeName="cx" repeatCount="indefinite" dur="1.6666666666666665s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-0.41666666666666663s"></animate>
</circle><circle cx="84" cy="50" r="10" fill="#83a79d">
<animate attributeName="r" repeatCount="indefinite" dur="1.6666666666666665s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;10;10;10" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-0.8333333333333333s"></animate>
<animate attributeName="cx" repeatCount="indefinite" dur="1.6666666666666665s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-0.8333333333333333s"></animate>
</circle><circle cx="16" cy="50" r="10" fill="#dae8e5">
<animate attributeName="r" repeatCount="indefinite" dur="1.6666666666666665s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;10;10;10" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-1.2499999999999998s"></animate>
<animate attributeName="cx" repeatCount="indefinite" dur="1.6666666666666665s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-1.2499999999999998s"></animate>
</circle>
<!-- [ldio] generated by https://loading.io/ --></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1,788 @@
<!-- 传入参数props-->
<!-- 必填项src,-->
<!-- 可选项width, height, speedList, cover, biBarrageXml-->
<template>
<div @contextmenu.prevent
ref="area"
:style="`--primaryColor: ${primaryColor}`"
:class="{'player-area': true, 'player-fullscreen': isFullscreen, 'cursor-lasting-static': isCursorStatic}"
>
<!-- 视频主体 -->
<video
ref="video"
class="player-video cursor-pointer"
@click="togglePlayStatus"
@keydown.f.prevent="toggleFullScreen"
@keydown.space.prevent="togglePlayStatus"
@keydown.left.prevent="backwardCurrentTime"
@keydown.right.prevent="forwardCurrentTime"
@keydown.up.prevent="increaseVolume"
@keydown.down.prevent="lowerVolume"
@keydown.esc.prevent="toggleFullScreen"
tabindex="0"
width="100%"
height="100%"
>
<source :src="src" />
</video>
<!-- 封面图 -->
<div v-if="cover!=null && isShowCover" class="player-cover">
<img :src="cover" :width="width" :height="height" alt=""/>
</div>
<!-- 弹幕 -->
<playerBarrageScreen
v-if="biBarrageXml != null"
:videoDom="videoDom"
:barrageTimelineStart="barrageTimelineStart"
:isPlaying="isPlaying"
:biBarrageXml="biBarrageXml"
/>
<!-- 加载动画 -->
<div v-show="isShowLoading" class="player-loading" @click="videoDom.focus({preventScroll: true})">
<img src="@/components/VideoPlayer/images/loading.svg" alt="loading"/>
</div>
<!-- 控制栏 -->
<div class="player-controls-container" @click="videoDom.focus({preventScroll: true})">
<div v-show="isShowVolumeHint" class="player-volumeHint">
<span class="player-volumeHint-text">当前音量:{{volumePercent}}%</span>
</div>
<div class="player-paused-state">
<svg
v-show="!isPlaying"
t="1596553913647"
class="player-controls-icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="13502"
width="32"
height="32"
>
<path
d="M897.143467 597.051733l-464.648534 311.5264c-46.976 31.488-110.592 18.944-142.08-28.023466A102.4 102.4 0 0 1 273.066667 823.5264V200.4736c0-56.5504 45.8496-102.4 102.4-102.4a102.4 102.4 0 0 1 57.028266 17.348267l464.64 311.5264c46.976 31.488 59.528533 95.104 28.032 142.08a102.4 102.4 0 0 1-28.023466 28.023466z"
p-id="13503"
fill="rgb(0,0,0,0.8)"
/>
</svg>
</div>
<div :class="{'player-controls': true, 'cursor-lasting-static': isCursorStatic}">
<div class="player-progress-bar">
<progressBar
:currentProgress="currentProgress"
v-on:updateProgress="updateProgressByClickBar"
v-on:getMouseDownStatus="getMouseDownStatusOfProgressBar"
:isDrag="isDrag"
width="100%"
height="4px"
></progressBar>
</div>
<div class="player-controls-bottom">
<div class="player-controls-bottom-left">
<div class="player-controls-btn cursor-pointer" @click="togglePlayStatus">
<svg
v-show="!isPlaying"
t="1596553913647"
class="player-controls-icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="13502"
width="32"
height="32"
>
<path
d="M897.143467 597.051733l-464.648534 311.5264c-46.976 31.488-110.592 18.944-142.08-28.023466A102.4 102.4 0 0 1 273.066667 823.5264V200.4736c0-56.5504 45.8496-102.4 102.4-102.4a102.4 102.4 0 0 1 57.028266 17.348267l464.64 311.5264c46.976 31.488 59.528533 95.104 28.032 142.08a102.4 102.4 0 0 1-28.023466 28.023466z"
p-id="13503"
fill="#ffffff"
/>
</svg>
<svg
v-show="isPlaying"
t="1596554115916"
class="player-controls-icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="17417"
width="32"
height="32"
>
<path
d="M268.97201558 114.31784297c73.21218086 0 132.56071902 59.34853814 132.56071901 132.560719v530.24287606c0 73.21218086-59.34853814 132.56071902-132.56071901 132.560719s-132.56071902-59.34853814-132.56071901-132.560719V246.87856197c0-73.21218086 59.34853814-132.56071902 132.56071901-132.560719z m486.05596884 0c73.21218086 0 132.56071902 59.34853814 132.56071901 132.560719v530.24287606c0 73.21218086-59.34853814 132.56071902-132.56071901 132.560719s-132.56071902-59.34853814-132.56071901-132.560719V246.87856197c0-73.21218086 59.34853814-132.56071902 132.56071901-132.560719z"
p-id="17418"
fill="#ffffff"
/>
</svg>
</div>
<div class="player-time">{{ currentTimeFormat }} / {{ fullTimeFormat }}</div>
</div>
<div class="player-controls-bottom-right">
<div class="player-controls-btn cursor-pointer btn-speed">
<span>{{currentSpeed === 1 ? '倍速' : `${currentSpeed}x`}}</span>
<div class="speed-control">
<ul class="speed-control-list">
<li
v-for="item in speedList"
:key="item"
@click="changeSpeed"
:data-value="item"
:class="{'current': currentSpeed === Number(item)}"
>{{ item }}x</li>
</ul>
</div>
</div>
<div class="player-controls-btn btn-volume">
<div class="cursor-pointer">
<svg
t="1596553801150"
class="player-controls-icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="12518"
width="32"
height="32"
>
<path
d="M308.971 657.987l150.28 165.279a16 16 0 0 0 11.838 5.236c8.837 0 16-7.163 16-16v-600.67a16 16 0 0 0-5.236-11.839c-6.538-5.944-16.657-5.463-22.602 1.075l-150.28 165.279A112 112 0 0 1 226.105 403H177c-17.673 0-32 14.327-32 32v154.333c0 17.674 14.327 32 32 32h49.105a112 112 0 0 1 82.866 36.654zM177 701.333c-61.856 0-112-50.144-112-112V435c0-61.856 50.144-112 112-112h49.105a32 32 0 0 0 23.676-10.472l150.28-165.28c35.668-39.227 96.383-42.113 135.61-6.445a96 96 0 0 1 31.418 71.028v600.671c0 53.02-42.98 96-96 96a96 96 0 0 1-71.029-31.417l-150.28-165.28a32 32 0 0 0-23.675-10.472H177z m456.058-348.336c-18.47-12.118-23.621-36.915-11.503-55.386 12.118-18.471 36.916-23.621 55.387-11.503C752.495 335.675 799 419.908 799 512c0 92.093-46.505 176.325-122.058 225.892-18.471 12.118-43.269 6.968-55.387-11.503-12.118-18.471-6.968-43.268 11.503-55.386C686.303 636.07 719 576.848 719 512c0-64.848-32.697-124.07-85.942-159.003z m92.93-137.323c-18.07-12.71-22.415-37.66-9.706-55.73s37.66-22.415 55.73-9.706C888.942 232.478 960 366.298 960 512s-71.058 279.522-187.988 361.762c-18.07 12.71-43.021 8.364-55.73-9.706-12.709-18.07-8.363-43.02 9.706-55.73C821.838 740.912 880 631.38 880 512c0-119.38-58.161-228.912-154.012-296.326z"
p-id="12519"
fill="#ffffff"
/>
</svg>
</div>
<div class="volume-control">
<div class="volume-control-wrap">
<div class="volume-text">{{ volumePercent }}</div>
<volumeBar
:currentVolume="currentVolume"
v-on:updateVolume="updateVolumeByClickBar"
width="4px"
></volumeBar>
</div>
</div>
</div>
<div class="player-controls-btn cursor-pointer" @click="toggleFullScreen">
<svg
v-show="!isFullscreen"
class="player-controls-icon"
t="1596553111831"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="10800"
width="32"
height="32"
>
<path
d="M0 232.732444A232.732444 232.732444 0 0 1 232.732444 0h558.535112A232.732444 232.732444 0 0 1 1024 232.732444v558.535112A232.732444 232.732444 0 0 1 791.267556 1024H232.732444A232.732444 232.732444 0 0 1 0 791.267556V232.732444z m232.732444-139.662222a139.662222 139.662222 0 0 0-139.662222 139.662222v558.535112a139.662222 139.662222 0 0 0 139.662222 139.662222h558.535112a139.662222 139.662222 0 0 0 139.662222-139.662222V232.732444a139.662222 139.662222 0 0 0-139.662222-139.662222H232.732444z"
p-id="10801"
fill="#ffffff"
/>
<path
d="M549.575111 245.845333c0-25.799111 20.935111-46.734222 46.734222-46.734222h116.821334A140.202667 140.202667 0 0 1 853.333333 339.313778v116.821333a46.734222 46.734222 0 0 1-93.468444 0v-116.821333c0-25.827556-20.906667-46.734222-46.734222-46.734222h-116.821334a46.734222 46.734222 0 0 1-46.734222-46.734223zM245.845333 549.546667c25.799111 0 46.734222 20.935111 46.734223 46.734222v116.821333c0 25.827556 20.906667 46.734222 46.734222 46.734222h116.821333a46.734222 46.734222 0 0 1 0 93.468445h-116.821333A140.202667 140.202667 0 0 1 199.111111 713.130667v-116.821334c0-25.799111 20.935111-46.734222 46.734222-46.734222z"
p-id="10802"
fill="#ffffff"
/>
</svg>
<svg
v-show="isFullscreen"
t="1596958879235"
class="player-controls-icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="2734"
width="32"
height="32"
>
<path
d="M0.739556 233.130667a232.391111 232.391111 0 0 1 232.391111-232.391111h557.738666a232.391111 232.391111 0 0 1 232.391111 232.391111v557.738666a232.391111 232.391111 0 0 1-232.391111 232.391111H233.130667a232.391111 232.391111 0 0 1-232.391111-232.391111V233.130667z m232.391111-139.434667a139.434667 139.434667 0 0 0-139.434667 139.434667v557.738666a139.434667 139.434667 0 0 0 139.434667 139.434667h557.738666a139.434667 139.434667 0 0 0 139.434667-139.434667V233.130667a139.434667 139.434667 0 0 0-139.434667-139.434667H233.130667z"
p-id="2735"
fill="#ffffff"
/>
<path
d="M601.088 186.652444c25.685333 0 46.506667 20.792889 46.506667 46.478223v96.796444c0 25.685333 20.792889 46.478222 46.478222 46.478222h96.796444a46.478222 46.478222 0 1 1 0 92.984889h-96.796444a139.434667 139.434667 0 0 1-139.463111-139.463111V233.130667c0-25.685333 20.821333-46.478222 46.478222-46.478223z m-414.435556 414.435556c0-25.656889 20.792889-46.478222 46.478223-46.478222h96.796444a139.434667 139.434667 0 0 1 139.463111 139.463111v96.796444a46.478222 46.478222 0 0 1-92.984889 0v-96.796444c0-25.685333-20.792889-46.478222-46.478222-46.478222H233.130667a46.478222 46.478222 0 0 1-46.478223-46.506667z"
p-id="2736"
fill="#ffffff"
/>
</svg>
<div
class="player-controls-btn-hint btn-fullscreen-hint"
>{{isFullscreen ? '退出全屏' : '进入全屏'}}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import volumeBar from "@/components/VideoPlayer/volume-bar.vue";
import progressBar from "@/components/VideoPlayer/progress-bar.vue";
import playerBarrageScreen from "@/components/VideoPlayer/player-barrage-screen.vue";
export default {
name: "barrage-videoplayer",
components: { volumeBar, progressBar, playerBarrageScreen },
props: {
// 倍速列表
speedList: {
type: Array,
default: function () {
return ["2.0", "1.5", "1.25", "1.0", "0.75", "0.5"];
},
},
// 视频链接
src: {
type: String,
default: null,
},
// 主题色
primaryColor: {
type: String,
default: "cornflowerblue",
},
// 封面图的链接
cover: {
type: String,
default: null,
},
// 弹幕的链接地址XML文件格式B站风格
biBarrageXml: {
type: String,
default: null,
},
initTime:{//默认从第几秒播放
type: Number,
default: null,
},
isDrag:{
type: Boolean,
default: null,
}
},
data() {
return {
timeFastBack : 15, //单位秒每次增减15秒
videoDom: null, //视频dom
isShowCover: true, // 是否显示封面
isFullscreen: false, // 是否处于全屏模式
isCursorStatic: false, // 鼠标是否长时间静止不动
isMousedownProgress: false, // 鼠标是否按下了进度条(并未松开)
isPlaying: false, // 是否正在播放
isShowLoading: false, // 是否显示加载框
isShowVolumeHint: false, // 是否显示音量提示条(键盘触发)
timeoutVolumeHint: 0, // 音量提示条多久ms后隐藏
timeoutControlsHint: 0, // 控制面板多久ms后隐藏
currentSpeed: 1.0, // 当前倍速
volumePercent: 100, // 当前音量百分比0-100仅用于文字显示
currentVolume: 1, // 当前音量0-1同时作用于当前音量条的长度
currentProgress: 0, // 当前播放进度0-1。同时作用于当前进度条的长度
currentTimeFormat: "00:00:00", // 当前播放进度的文字
fullTimeFormat: "00:00:00", // 视频总长度的文字
barrageTimelineStart: 0, // 弹幕时间轴的起始时间点(手动调整进度条触发更新)
isInit:false, // 是否初始化过
};
},
created() {},
mounted() {
this.videoDom = this.$refs.video;
this.videoDom.focus({preventScroll: true});
let speedValue=localStorage.getItem('boe_video_speed');
if(speedValue){
//console.log(speedValue,'speedValue');
this.currentSpeed=Number(speedValue);
this.videoDom.playbackRate=this.currentSpeed;
}
setInterval(() => {
// 定时更新进度条
if (this.isPlaying && !this.isMousedownProgress) {
this.currentProgress =
this.videoDom.currentTime / this.videoDom.duration;
}
// 定时更新进度的文字显示
this.updateProgressText();
// 音量提示面板的定时隐藏
if (this.timeoutVolumeHint == null) {
// 当定时为null直接跳过
} else if (this.timeoutVolumeHint === 0) {
this.isShowVolumeHint = false;
this.isCursorStatic = false;
this.timeoutVolumeHint = null;
} else if (this.timeoutVolumeHint > 0) {
this.timeoutVolumeHint -= 1000;
} else {
this.timeoutVolumeHint = 0;
}
// 总控制面板的定时隐藏
if (
this.timeoutControlsHint == null ||
this.currentProgress === 0 ||
this.currentProgress === 1
) {
// 当定时为null、视频还未播放或播放完毕的时候直接跳过
} else if (this.timeoutControlsHint === 0) {
this.isCursorStatic = true;
this.timeoutControlsHint = null;
} else if (this.timeoutControlsHint > 0) {
this.timeoutControlsHint -= 1000;
} else {
this.timeoutControlsHint = 0;
}
// 根据视频的readyState判断下一帧是否已加载并控制loading的显示
this.isShowLoading = this.videoDom.readyState < 3;
}, 1000);
// 视频dom监听器用于控制鼠标的显示
this.videoDom.addEventListener("mousemove", () => {
this.isCursorStatic = false;
this.timeoutControlsHint = 2000;
});
// 监听全屏事件的变化,保存数据
window.addEventListener("fullscreenchange", () => {
this.isFullscreen = this.isFullScreen();
});
},
methods: {
/* 切换播放状态
*/
togglePlayStatus() {
if (this.videoDom.paused) {
if(!this.isInit && this.initTime != null && this.initTime > 0){
this.videoDom.currentTime = this.initTime;
this.barrageTimelineStart = this.initTime;
this.updateProgressBySetTime(this.initTime);
this.updateProgressText();
}
this.videoDom.play();
this.isPlaying = true;
this.isInit = true;
this.$emit('onPlayerPlay', {});//播放(播放时会调用)
} else {
this.videoDom.pause();
this.isPlaying = false;
this.$emit('onPlayerPause', {})//暂停(暂停时调用)
}
this.isShowCover = false;
},
/* 更新视频进度的文字显示
*/
updateProgressText() {
this.currentTimeFormat = this.secondTimeFormat(
this.videoDom.currentTime
);
this.fullTimeFormat = this.secondTimeFormat(this.videoDom.duration);
},
/* 时间格式化秒格式化成xx:xx:xx
*/
secondTimeFormat(second) {
let result = parseInt(second);
let h =
Math.floor(result / 3600) < 10
? "0" + Math.floor(result / 3600)
: Math.floor(result / 3600);
let m =
Math.floor((result / 60) % 60) < 10
? "0" + Math.floor((result / 60) % 60)
: Math.floor((result / 60) % 60);
let s =
Math.floor(result % 60) < 10
? "0" + Math.floor(result % 60)
: Math.floor(result % 60);
result = `${h}:${m}:${s}`;
return result;
},
/* 前进视频播放进度
*/
forwardCurrentTime() {
let newCurrentTime = this.videoDom.currentTime + this.timeFastBack;
this.videoDom.currentTime = newCurrentTime;
this.barrageTimelineStart = newCurrentTime;
this.updateProgressBySetTime(newCurrentTime);
},
/* 后退视频播放进度
*/
backwardCurrentTime() {
let newCurrentTime = this.videoDom.currentTime - this.timeFastBack;
this.videoDom.currentTime = newCurrentTime;
this.barrageTimelineStart = newCurrentTime;
this.updateProgressBySetTime(newCurrentTime);
},
/* 获取鼠标是否按下了进度条
*/
getMouseDownStatusOfProgressBar(value) {
this.isMousedownProgress = value;
},
/* 点击进度条更新视频播放进度
*/
updateProgressByClickBar(value) {
let duration = this.videoDom.duration;
this.currentProgress = value;
let new_current_time = Math.round(value * duration);
this.barrageTimelineStart = new_current_time;
this.videoDom.currentTime = new_current_time;
},
/* 通过新的播放时间更新视频播放进度
*/
updateProgressBySetTime(newCurrentTime) {
this.currentProgress = newCurrentTime / this.videoDom.duration;
},
/* 提高视频音量
*/
increaseVolume() {
this.isShowVolumeHint = true;
this.timeoutVolumeHint = 2000;
let nowVolume = this.videoDom.volume;
if (nowVolume >= 0.9) {
this.videoDom.volume = 1;
this.currentVolume = 1;
} else {
let newVolume = this.videoDom.volume + 0.1;
this.videoDom.volume = newVolume;
this.currentVolume = newVolume;
}
},
/* 降低视频音量
*/
lowerVolume() {
this.isShowVolumeHint = true;
this.timeoutVolumeHint = 2000;
let nowVolume = this.videoDom.volume;
if (nowVolume <= 0.1) {
this.videoDom.volume = 0;
this.currentVolume = 0;
} else {
let newVolume = this.videoDom.volume - 0.1;
this.videoDom.volume = newVolume;
this.currentVolume = newVolume;
}
},
/* 视频倍速播放
*/
changeSpeed(e) {
// 获取选择的倍速
let value = e.currentTarget.dataset.value;
localStorage.setItem('boe_video_speed',value);
// 应用视频倍速
this.videoDom.playbackRate = value;
// 标记变更后的倍速,用于显示文字
this.currentSpeed = Number(value);
},
/* 点击音量条后更新音量value范围0-1
*/
updateVolumeByClickBar(value) {
this.videoDom.volume = value;
this.currentVolume = value;
},
/* 切换“全屏”和“非全屏”模式
*/
toggleFullScreen() {
let element = this.$refs.area;
if (!this.isFullScreen()) {
if (element.requestFullscreen) {
element.requestFullscreen();
} else if(element.webkitRequestFullscreen) {
element.webkitRequestFullscreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
} else if (element.oRequestFullscreen) {
element.oRequestFullscreen();
}
this.isFullscreen = true;
this.$emit('onFullscreen',true);//全屏
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.oRequestFullscreen) {
document.oCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
this.isFullscreen = false;
this.$emit('onFullscreen',false);//全屏
}
},
/* 判断是否进入了全屏模式
*/
isFullScreen: function () {
return !!(
document.fullscreen ||
document.mozFullScreen ||
document.webkitIsFullScreen ||
document.webkitFullScreen ||
document.msFullScreen
);
},
},
watch: {
currentVolume: function () {
// 根据当前音量大小设置显示的音量文字
this.volumePercent = Math.round(this.currentVolume * 100);
},
currentProgress: function () {
// 进度条到终点时修改播放状态
//console.log('播放中', this.videoDom.currentTime)
this.$emit('onPlayerPlaying', this.videoDom.currentTime,this.videoDom.duration)
if (this.currentProgress === 1) {
this.isPlaying = false;
this.$emit('onPlayerEnded', {})
}
},
src: function () {
// 当视频地址变更时,重载视频
this.videoDom.load();
},
},
};
</script>
<style scoped>
.player-area {
width: 100%;
height: 100%;
position: relative;
display: inline-block;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.cursor-pointer {
cursor: pointer;
}
.cursor-lasting-static .cursor-pointer {
cursor: none;
}
.cursor-lasting-static.player-controls {
visibility: hidden;
opacity: 0;
}
.player-video {
outline: none;
vertical-align: middle;
object-fit: contain;
}
.player-cover {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow: hidden;
pointer-events: none;
}
.player-loading {
position: absolute;
top: 50%;
bottom: 50%;
left: 50%;
right: 50%;
width: 80px;
height: 80px;
transform: translate(-50%, -50%);
}
.player-loading img {
fill: #fff;
width: inherit;
height: inherit;
}
.player-paused-state {
position: absolute;
pointer-events: none;
top: 0;
left: 2%;
transform: translate(0, -100%);
border-radius: 10px;
box-shadow: 0 0 20px #bbbbbb;
background: rgba(255, 255, 255, 0.8);
}
.player-paused-state svg {
/* margin: 0.4rem 1rem; */
margin: 1rem 2em 0.8em 2em;
}
.player-controls-container {
position: absolute;
bottom: 0;
width: 100%;
}
.player-volumeHint {
position: absolute;
top: 0;
left: 12px;
pointer-events: none;
transform: translate(0, -100%);
border-radius: 4px;
background: rgba(0, 0, 0, 0.8);
}
.player-volumeHint-text {
position: relative;
display: inline-block;
padding: 0.4rem 0.6rem;
color: #fff;
font-size: 1rem;
}
.player-controls {
visibility: visible;
opacity: 1;
position: relative;
display: block;
transition: visibility 0.3s, opacity 0.3s;
background-image: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.6));
}
.player-controls-container:hover .player-controls {
visibility: visible;
opacity: 1;
}
.player-controls-container:hover .cursor-pointer {
cursor: pointer;
}
.player-controls-bottom {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.player-controls-bottom-left {
padding: 0.6rem 0.4rem;
display: inline-flex;
align-items:center;
justify-content:center;
}
.player-controls-bottom-right {
display: inline-flex;
padding: 0.6rem 0.4rem;
align-items:center;
justify-content:center;
}
.player-controls-btn {
position: relative;
display: inline-block;
color: #e5e5e5;
padding: 0 0.4rem;
transition: color 0.3s;
height: 22px;
}
.player-controls-btn .player-controls-icon {
height: 22px;
transition: height 0.3s;
}
.player-controls-btn svg path {
fill: #e5e5e5;
transition: fill 0.2s;
}
.player-controls-btn:hover svg > path {
fill: #ffffff;
}
.player-controls-btn:hover {
color: #fff;
}
.player-controls-btn:hover .player-controls-btn-hint {
visibility: visible;
opacity: 1;
}
.player-controls-btn-hint {
visibility: hidden;
opacity: 0;
position: absolute;
top: 0;
left: 50%;
white-space: nowrap;
padding: 0.4rem 0.6rem;
border-radius: 4px;
background: rgba(21, 21, 21, 0.8);
transition: opacity 0.3s, visibility 0s;
transform: translate(-50%, -200%);
}
.btn-fullscreen-hint {
font-size: 12px;
margin-left: -16px;
}
.player-time {
display: inline-block;
color: #e5e5e5;
height: 16px;
}
.btn-speed:hover .speed-control {
visibility: visible;
}
.speed-control {
visibility: hidden;
position: absolute;
top: 0;
left: 50%;
padding-bottom: 24px;
transition: visibility 0.3s;
transform: translate(-50%, -100%);
}
.speed-control .speed-control-list {
list-style: none;
color: #e5e5e5;
width: 50px;
font-size: 12px;
text-align: center;
padding: 0;
margin: 0;
overflow: hidden;
border-radius: 4px;
background: rgba(21, 21, 21, 0.8);
}
.speed-control .speed-control-list li {
position: relative;
display: block;
height: 25px;
line-height: 25px;
}
.speed-control .speed-control-list li:hover {
color: #fff;
background: rgba(99, 99, 99, 0.8);
}
.speed-control .speed-control-list li.current {
color: var(--primaryColor);
}
.btn-volume:hover .volume-control {
visibility: visible;
}
.volume-control {
visibility: hidden;
position: absolute;
top: 0;
left: 50%;
padding: 24px 10px;
transition: visibility 0.3s;
transform: translate(-50%, -100%);
}
.volume-control-wrap {
display: flex;
text-align: center;
flex-direction: column;
margin: auto;
width: 35px;
height: 120px;
padding: 0.6rem 0 1rem;
background: rgba(21, 21, 21, 0.8);
border-radius: 4px;
}
.volume-text {
font-size: 12px;
color: #fff;
margin-bottom: 0.5rem;
}
@media (device-width: 100vw) {
.player-controls-btn .player-controls-icon {
/* height: 26px; */
}
}
@media (min-width: 1000px) {
.player-loading {
width: 100px;
height: 100px;
}
}
</style>

View File

@@ -0,0 +1,187 @@
<template>
<div class="player-barrage-wrap">
<div v-for="n in channelAmount" ref="channels" class="player-barrage-layer" :key="`channel-${(n -1)}`">
</div>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "player-barrage-screen",
props: {
// 是否正在播放
isPlaying: {
type: Boolean,
default: false,
},
// 当前播放进度
currentTime: {
type: Number,
default: 0,
},
videoDom: null,
// 弹幕时间轴的初始时间(手动干预进度条会触发更新)
barrageTimelineStart: {
type: Number,
default: 0,
},
// 弹幕的链接地址XML文件格式B站风格
biBarrageXml: {
type: String,
default: null,
}
},
data() {
return {
barragedTag: 0, // 标记上一条弹幕的索引
barrageList: [], // 弹幕列表
channelAmount: 10, // 弹幕通道数量
};
},
mounted() {
this.requestBarrageList();
// 测试弹幕
setInterval(() => {
if (this.isPlaying) {
let info = this.barrageList[this.barragedTag];
if(info){
console.log(info);
if (info.start_time < this.videoDom.currentTime) {
// 标记下一条弹幕的索引
this.barragedTag++;
// if (info.mode <= 3) {
this.createBarrage(this.barrageList[this.barragedTag]);
// }
}
}
}
}, 10);
},
methods: {
/* 获取并处理排序弹幕列表
*/
requestBarrageList() {
// 获取弹幕列表
axios({
method: "get",
url: this.biBarrageXml,
data: {},
}).then((res) => {
let xmlDoc = new DOMParser().parseFromString(res.data, "text/xml");
let elements = xmlDoc.getElementsByTagName("d");
let array = [];
for (let n = 0, len = elements.length; n < len; n++) {
let p_attr_list = elements[n].getAttribute("p").split(",");
// 弹幕的信息
let color = Number(p_attr_list[3]).toString(16);
let info = {
content: elements[n].innerHTML,
start_time: Number(p_attr_list[0]),
mode: Number(p_attr_list[1]),
font_size: p_attr_list[2],
font_color: color,
timestamp: p_attr_list[4],
barrage_pool: p_attr_list[5],
user_hash: p_attr_list[6],
row_id: p_attr_list[7],
};
array.push(info);
}
// 写个冒泡过渡一下。。
for (let i = 0, len = array.length; i < len; i++) {
for (let j = 0, len_cache = len - i - 1; j < len_cache; j++) {
if (array[j].start_time > array[j + 1].start_time) {
let array_cache = array[j];
array[j] = array[j + 1];
array[j + 1] = array_cache;
}
}
}
this.barrageList = array;
console.log( this.barrageList);
});
},
/* 新建一个弹幕
*/
createBarrage(info) {
let dom = document.createElement("span");
dom.innerText = info.content;
// dom.setAttribute("class", "barrage");
dom.style.animation = "barrage 5s linear 0s";
dom.style.left = "100%";
dom.style.fontSize = `${info.font_size}px`;
dom.style.color = `#${info.font_color}`;
dom.style.whiteSpace = "nowrap";
dom.style.textShadow =
"#000 1px 0px 1px, #000 0px 1px 1px, #000 0px -1px 1px, #000 -1px 0px 1px";
dom.style.position = "absolute";
//动画过渡完之后清除掉弹幕dom
dom.addEventListener("animationend", () => {
dom.removeEventListener("animationend", this, false);
dom.innerHTML = "";
dom.parentNode.removeChild(dom);
dom.remove();
dom = null;
});
this.$refs["channels"][Math.round(Math.random() * 9)].append(dom);
},
},
watch: {
/* 更新弹幕
*/
biBarrageXml: function () {
this.requestBarrageList();
},
/* 弹幕时间轴的初始时间。
* 在手动改变进度条的时候触发,遍历弹幕列表获取下一条弹幕的索引
*/
barrageTimelineStart: function () {
let list = this.barrageList;
let barrage_start = this.barrageTimelineStart;
for (let n = 0; n < list.length; n++) {
if (list[n].start_time > barrage_start) {
this.barragedTag = n;
break;
}
}
},
},
};
</script>
<style scoped>
.player-barrage-wrap {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
cursor: pointer;
pointer-events: none;
overflow: hidden;
}
.player-barrage-layer {
position: relative;
height: 2rem;
line-height: 2rem;
display: block;
z-index: 1;
}
</style>
<style>
@keyframes barrage {
from {
left: 100%;
transform: translate3d(0, 0, 0);
}
to {
left: 0;
transform: translate3d(-100%, 0, 0);
}
}
</style>

View File

@@ -0,0 +1,154 @@
/**
$emit: updateProgress, -> 调整进度条后触发的事件传递参数currentProgress
getMouseDownStatus, -> 获取是否鼠标按下的状态传递参数true/false
props: width, height,
currentProgress, -> 当前进度范围0-1
*/
<template>
<div
class="progress-bar"
:style="{width: width}"
@mousedown.left="down"
@mousemove="move"
@mouseup="up"
>
<div class="progress-full" :style="{height: height}">
<div class="progress-current" :style="{width: currentProgress * 100 + '%'}"></div>
</div>
</div>
</template>
<script>
export default {
name: "progress-bar",
props: {
width: {
type: String,
default: "100%",
},
height: {
type: String,
default: "4px",
},
// 当前进度范围0-1
currentProgress: {
type: Number,
default: 0,
},
isDrag:{
type: Boolean,
default: true,
}
},
data() {
return {
is_mousedown_progress: false, //是否点击了进度条并保持点击状态
current_width_px: 0, // 当前进度条的像素长度
init_clientX: 0, //初始进度的偏移量,相对于视口
dom_full: null, //完整进度条的dom即进度条背景
};
},
mounted() {
console.log(this.isDrag,'isDrag');
//初始化一些固定数据
let dom_full = this.$el.getElementsByClassName("progress-full")[0];
this.dom_full = dom_full;
//绑定全局监听器
window.addEventListener("mousemove", (e) => {
if (this.is_mousedown_progress) {
this.move(e);
}
});
window.addEventListener("mouseup", (e) => {
this.up(e);
});
},
beforeDestroy() {
//
},
methods: {
down(e) {
if(this.isDrag) {
this.$emit("getMouseDownStatus", true);
this.is_mousedown_progress = true;
// 获取完整进度条的clientXdom左上角
let init_clientX = this.dom_full.getBoundingClientRect().left;
// 计算调整后的当前进度条的长度
this.current_width_px = e.clientX - init_clientX;
// 设置当前的播放进度(同时作用于当前进度条的样式)
let current =
(e.clientX - init_clientX) / this.dom_full.clientWidth;
this.$emit("updateProgress", current);
}
},
move(e) {
if (this.is_mousedown_progress && this.isDrag) {
let init_clientX = this.dom_full.getBoundingClientRect().left;
this.current_width_px = e.clientX - init_clientX;
let current =
(e.clientX - init_clientX) / this.dom_full.clientWidth;
this.$emit("updateProgress", current);
}
},
up() {
if (this.is_mousedown_progress && this.isDrag) {
// 标记鼠标不处于按下的状态了
this.is_mousedown_progress = false;
// 松开鼠标后即调整进度条后此时的进度0-1
let current =
this.current_width_px / this.dom_full.clientWidth;
this.$emit("updateProgress", current);
this.$emit("getMouseDownStatus", false);
}
},
},
};
</script>
<style scoped>
.progress-bar {
position: relative;
cursor: pointer;
padding: 2px 0;
transition: height 0.2s;
overflow: hidden;
}
.progress-bar:hover .progress-full {
height: 6px !important;
}
.progress-full {
position: relative;
display: inline-block;
width: 100%;
background: rgb(201, 201, 201);
transition: height 0.3s;
}
.progress-current {
position: absolute;
left: 0;
top: 0;
height: inherit;
width: 50%;
display: inline-block;
background-color: var(--primaryColor);
}
.progress-current::after {
content: "";
position: absolute;
display: inline-block;
right: 0;
top: 50%;
bottom: 50%;
width: 15px;
height: 15px;
border-radius: 100%;
background: #fff;
transform: translate(50%, -50%);
opacity: 0;
transition: opacity 0.3s;
}
.progress-bar:hover .progress-current::after {
opacity: 1;
}
</style>

View File

@@ -0,0 +1,130 @@
/**
$emit: updateVolume -> 更新音量参数为一个0-1的音量值
props: width, height,
currentVolume, -> 当前音量0-1
*/
<template>
<div
class="volume-bar"
:style="{width: '100%', height: height}"
@mousedown.left.stop="down"
@mousemove.stop="move"
@mouseup.stop="up"
>
<div class="volume-full" :style="{width: width, height: height}">
<div
class="volume-current volume-ball"
:style="{width: '100%', height: currentVolume * 100 + '%'}"
></div>
</div>
</div>
</template>
<script>
export default {
name: "volumeBar",
props: {
width: {
type: String,
default: "100%",
},
height: {
type: String,
default: "100%",
},
//当前音量0-1
currentVolume: {
type: Number,
default: 1
}
},
data() {
return {
is_click_bar: false, // 是否点击了音量条
full_height: null, //音量条的总高度(像素)
dom_volume_full: null, //总音量条的dom
full_bar_client_top: 0, //满载的音量条相对于视口的高度
};
},
mounted() {
this.dom_volume_full = this.$el.getElementsByClassName("volume-full")[0];
window.addEventListener("mousemove", (e) => {
if (this.is_click_bar) {
this.move(e);
}
});
window.addEventListener("mouseup", (e) => {
this.up(e);
});
},
methods: {
down(e) {
this.is_click_bar = true;
//赋值高度备用
let full_height = this.dom_volume_full.clientHeight;
this.full_height = full_height;
this.$emit('updateVolume', 1 - e.offsetY / this.full_height)
},
move(e) {
//满载的音量条相对于视口的高度
let full_bar_client_top = this.dom_volume_full.getBoundingClientRect().top;
this.full_bar_client_top = full_bar_client_top;
//当前点击位置距离视口的高度
let clickY = e.clientY;
//偏移量
let offsetY = clickY - full_bar_client_top;
//当鼠标按下的时候才进行更新数据
if (this.is_click_bar ) {
let new_volume = 1 - (offsetY / this.full_height);
if (new_volume > 1) {
this.$emit("updateVolume", 1)
} else if (new_volume < 0) {
this.$emit("updateVolume", 0)
} else {
this.$emit("updateVolume", new_volume)
}
}
},
up() {
this.is_click_bar = false;
},
},
};
</script>
<style>
.volume-bar {
display: inline-block;
cursor: pointer;
text-align: center;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.volume-full {
display: inline-block;
position: relative;
background: rgb(180, 180, 180);
border-radius: 10px;
}
.volume-current {
position: absolute;
bottom: 0;
background-color: var(--primaryColor);
pointer-events: none;
border-radius: 10px;
}
.volume-current::after {
content: "";
display: inline-block;
position: absolute;
/* top: 0; */
width: 12px;
height: 12px;
border-radius: 100%;
background: #fff;
transform: translate(-50%, -50%);
}
</style>