代码拉取完成,页面将自动刷新
同步操作将从 云帆/jsmpeg-player 强制同步,此操作会覆盖自 Fork 仓库以来所做的任何修改,且无法恢复!!!
确定后同步将在后台操作,完成时将刷新页面,请耐心等待。
<template>
<div class="jsmpeg-player"
@mouseenter="handlePlayerMouseEnter"
@mouseleave="handlePlayerMouseLeave">
<div class="jsmpeg-header"
:class="{'is-show':showTitle}">
<slot v-if="$slots.title"
name="title" />
<span v-else-if="title"
class="jsmpeg-title">
{{title}}
</span>
<div class="recording-title"
v-if="isRecording">
<template v-if="showTitle">
<div class="icon" />REC {{recordingDurationLabel}}
</template>
<template v-else>
<div class="icon"
:class="recordingDuration%2==0?'is-hide':''" />REC
</template>
</div>
<button v-if="showCloseBtn"
class="close-btn el-icon-close"
title="关闭"
@click="$emit('close')">
</button>
</div>
<div class="jsmpeg-canvas__wrap"
ref="canvas-wrap"
v-loading="loading"
element-loading-text="拼命加载中..."
@mousemove.passive="handleCanvasMouseMove"
@click="handleCanvasClick"
@dblclick="toggleFullscreen">
<!-- <canvas class="jsmpeg-canvas"
ref="canvas" /> -->
<template v-if="!loading&&flags.noSignal">
<div v-if="!$slots['no-signal']"
class="no-signal-text">
无信号
</div>
<slot name="no-signal"></slot>
</template>
</div>
<div class="jsmpeg-toolbar"
v-if="withToolbar"
:class="{'is-show':player&&isPlayerHover}"
@mouseenter="handleToolbarMouseEnter"
@mouseleave="handleToolbarMouseLeave">
<button class="toolbar-btn play-btn"
:class="paused?'el-icon-video-play paused':'el-icon-video-pause'"
:title="paused?'播放':'暂停'"
@click="handleToolbarBtnClick('play')">
</button>
<button class="toolbar-btn stop-btn ut-icon-stop"
title="停止"
@click="handleToolbarBtnClick('stop')">
</button>
<button class="toolbar-btn volume-btn"
:class="isMuted?'ut-icon-muted':'ut-icon-volume'"
title="音量"
v-popover:popover-volume
@click="handleToolbarBtnClick('mute')">
</button>
<div class="progress-bar">
<span v-if="showDuration"
class="current-time">
{{currentTimeLabel}}
</span>
</div>
<!-- <button class="snapshot-btn"
title="画中画"
@click="requesPip">
<i class="el-icon-copy-document"></i>
</button> -->
<button class="toolbar-btn snapshot-btn el-icon-camera"
title="截图"
@click="handleToolbarBtnClick('snapshot')">
</button>
<button class="toolbar-btn recording-btn ut-icon-recording"
:class="isRecording?'is-recording':''"
:title="isRecording?'停止录制':'录制'"
@click="handleToolbarBtnClick('recording')">
</button>
<button class="toolbar-btn setting-btn el-icon-setting"
title="设置"
v-popover:popover-setting>
</button>
<button class="toolbar-btn fullscreen-btn"
:class="isFullscreen?'ut-icon-exitfullscreen':'ut-icon-fullscreen'"
:title="isFullscreen?'取消全屏':'全屏'"
@click="handleToolbarBtnClick('fullscreen')">
</button>
</div>
<div class="overlayers">
<template v-if="withToolbar">
<el-popover popper-class="jsmpeg-popover popover-setting"
ref="popover-setting"
trigger="hover"
placement="top-end"
:visible-arrow="popoverVisibleArrow"
:append-to-body="false">
<!-- <div class="setting-item">
<span class="label">禁用WebGL</span>
<div class="input__wrap">
<el-switch class="input"
v-model="playerSettings.disableGl">
</el-switch>
</div>
</div> -->
<!-- <div class="setting-item"
highlight>
<span class="label">后台播放</span>
<div class="input__wrap">
<el-switch class="input"
v-model="playerSettings.backgroudPlay"
@change="settingPlayer('pauseWhenHidden',!$event)">
</el-switch>
</div>
</div> -->
<div class="setting-item"
highlight>
<span class="label">自动拉伸</span>
<div class="input__wrap">
<el-switch class="input"
v-model="playerSettings.autoStretch"
@change="settingPlayer('autoStretch',$event)">
</el-switch>
</div>
</div>
<div class="setting-item"
highlight>
<span class="label">旋转画面</span>
<div class="input__wrap">
<button class="toolbar-btn ut-icon-rotate-left-90"
title="向左旋转90度"
@click="rotate(-90,true)"></button>
<button class="toolbar-btn ut-icon-rotate-right-90"
title="向右旋转90度"
@click="rotate(90,true)"></button>
</div>
</div>
<!-- <div class="setting-item">
<span class="label">test</span>
<div class="input__wrap">
<el-button class="input"
@click="player.stop(true)">
</el-button>
</div>
</div> -->
</el-popover>
<el-popover popper-class="jsmpeg-popover popover-volume"
ref="popover-volume"
trigger="hover"
placement="top"
:visible-arrow="popoverVisibleArrow"
:append-to-body="false">
<div class="volume-value">{{volumePercent}}</div>
<el-slider v-model="volume"
vertical
height="120px"
:max="1"
:min="0"
:step="0.01"
:show-tooltip="false"
:marks="{
0 : '',
0.5 : '',
1 : '',
}"
@change="$emit('volume-change', volume)">
</el-slider>
</el-popover>
</template>
</div>
</div>
</template>
<script>
import JSMpeg from '../jsmpeg'
const fullscreen = {
get element() {
return (
document.fullscreenElement ??
document.msFullscreenElement ??
document.mozFullScreenElement ??
document.webkitFullscreenElement ??
null
)
},
/**
*
* @param {HTMLElement} el
* @param {()=>void} exitCb
*/
request(el, exitCb) {
if (el instanceof HTMLElement) {
const cb = (ev) => {
console.log('fullscreen -> resize ', ev)
el.scrollIntoView({
block: 'nearest',
behavior: 'smooth'
})
if (!this.element) {
exitCb?.()
window.removeEventListener('resize', cb)
console.log('此元素请求的全屏已退出', el)
}
}
window.addEventListener('resize', cb, false)
const requestMethod =
el.requestFullScreen ?? //W3C
el.webkitRequestFullScreen ?? //FireFox
el.mozRequestFullScreen ?? //Chrome等
el.msRequestFullScreen //IE11
requestMethod?.call(el)
}
},
exit(el) {
if (!this.isFullscreen()) return
const exitMethod =
document.exitFullscreen ?? //W3C
document.mozCancelFullScreen ?? //FireFox
document.webkitCancelFullScreen ?? //Chrome等
document.msExiFullscreen //IE11
exitMethod?.call(document)
},
isFullscreen() {
return this.element != null
}
}
export default fullscreen
function prefixZero(num) {
return (num >= 10 ? '' : '0') + num
}
function getTimeLabel(time) {
const seconds = parseInt(time % 60),
minutes = parseInt(time / 60),
hours = parseInt(minutes / 60)
return time < 3600
? `${prefixZero(minutes)}:${prefixZero(seconds)}`
: `${prefixZero(hours)}:${prefixZero(minutes)}:${prefixZero(seconds)}`
}
const computed = {
paused() {
return this.player?.paused ?? true
},
volume: {
set(val) {
if (!this.player) return
if (val >= 1) {
this.player.volume = 1
} else if (val <= 0) {
this.player.volume = 0
} else {
this.player.volume = val
}
if (this.player.volume === 0) {
this.$emit('muted', this.player.volume)
}
},
get() {
return this.player?.volume ?? 100
}
},
volumePercent() {
return parseInt(this.volume * 100)
},
currentTime: {
set(val) {
this.player.currentTime = val
},
get() {
return this.player?.currentTime ?? 0
}
},
currentTimeLabel() {
return getTimeLabel(this.currentTime)
},
isMuted() {
return this.volume === 0
},
isRecording() {
return this.player && this.player.isRecording
},
recordingDuration() {
return this.player ? this.player.recordingDuration : 0
},
recordingDurationLabel() {
return getTimeLabel(this.recordingDuration)
},
showCloseBtn() {
return this.closeable && !this.isFullscreen
},
showTitle() {
return this.isPlayerHover && (this.$slots.title || this.title || this.showCloseBtn)
}
}
const defaultOptions = () => ({
/** 是否循环播放视频(仅静态文件)。默认true */
autoplay: true,
/** 是否解码音频。默认true */
audio: true,
/** 是否解码视频。默认true */
video: true,
/** 一个图像的URL,用来在视频播放之前作为海报显示。 */
poster: null,
/** 是否禁用后台播放,当TAB处于非活动状态时是否暂停播放。注意,浏览器通常会在非活动标签中限制JS。默认true */
pauseWhenHidden: true,
/** 是否禁用WebGL,始终使用Canvas2D渲染器。默认.false */
disableGl: false,
/** 是否禁用WebAssembly并始终使用JavaScript解码器。默认false */
disableWebAssembly: false,
/** WebGL上下文是否创建-必要的“截图”通过。默认false */
preserveDrawingBuffer: true,
/** 是否以块的形式加载数据(仅静态文件)。当启用时,回放可以在完整加载源之前开始 */
progressive: true,
/** 当不需要回放时是否推迟加载块。默认=progressive */
throttled: true,
/** 使用时,以字节为单位加载的块大小。默认(1 mb)1024*1024 */
chunkSize: 1024 * 1024,
/** 是否解码并显示视频的第一帧。设置画布大小和使用框架作为“海报”图像很有用。这在使用或流源时没有影响。默认true */
decodeFirstFrame: false,
/** 流媒体时,以秒为单位的最大排队音频长度。 */
maxAudioLag: 0.25,
/** 流媒体时,视频解码缓冲区的字节大小。默认的512 * 1024 (512 kb)。对于非常高的比特率,您可能需要增加此值。 */
videoBufferSize: 1024 * 1024,
/** 流媒体时,音频解码缓冲区的字节大小。默认的128 * 1024 (128 kb)。对于非常高的比特率,您可能需要增加此值。 */
audioBufferSize: 256 * 1024
})
export default {
name: 'jsmpeg-player',
props: {
url: String,
title: String,
options: {
type: Object,
default: defaultOptions
},
/** 是否可关闭(单击关闭按钮,仅抛出事件) */
closeable: Boolean,
/** 是否处于后台,如el-tabs的切换,路由的切换等 */
inBackground: Boolean,
/** 是否现实持续播放时间 */
showDuration: {
type: Boolean,
default: true
},
/** 默认静音 */
defaultMute: Boolean,
/** 是否需要工具栏 */
withToolbar: {
type: Boolean,
default: true
},
popoverVisibleArrow: {
type: Boolean,
default: true
},
width: [String, Number],
height: [String, Number]
},
inject: {
rootTabs: {
default: ''
}
},
mounted() {
if (this.rootTabs) {
this.rootTabs.$on('tab-click', (tab) => {
try {
// 处理el-tabs切换标签时,el-table右侧可能出现空白的Bug
if (!tab.$el?.contains(this.$el)) {
this.intoBackground()
}
} catch (error) {}
})
}
window.addEventListener('unload', () => {
this.destroyPlayer()
})
this.init()
},
destroyed() {
this.destroyPlayer()
},
data() {
return {
loading: false,
flags: {
/**
* 是否处于无信号状态
* 1.当流中断事件触发后,15秒后还没有收到ws消息
* 2.ws关闭事件触发
*/
noSignal: false,
/** 是否已获取到视频分辨率 */
gotResolution: false
},
/** @type {import('@uiot-core/class/jsmpeg/jsmpeg').JSMpegPlayer} */
player: null,
isPlayerHover: false,
canvasMouseMoveTimer: null,
isFullscreen: false,
lastVolume: 0,
playerSettings: {
disableGl: false,
/** canvas旋转角度 */
rotationAngle: 0,
backgroudPlay: false,
autoStretch: true
},
timers: {
noSignal: null
}
}
},
computed: computed,
watch: {
url(nval) {
// this.rotate(0)
// if (this.player) {
// this.player.setUrl(nval)
// } else {
// this.initPlayer()
// }
this.player?.destroy()
if (this.url == null || this.url == '') {
this.player = null
} else {
this.initPlayer()
}
},
options: {
deep: true,
handler() {
this.destroyPlayer()
this.initPlayer()
}
},
inBackground(nval) {
if (nval) {
this.intoBackground()
} else {
this.intoFront()
}
}
},
methods: {
init() {
this.initPlayer()
},
initPlayer() {
if (!this.url) return
this.loading = true
this.player = new JSMpeg.Player(this.url, {
contianer: this.$refs['canvas-wrap'],
...this.options,
onVideoDecode: (decoder, time) => {
this.$emit('video-decode', decoder, time)
},
onAudioDecode: (decoder, time) => {
this.$emit('audio-decode', decoder, time)
},
onPlay: (player) => {
this.loading = false
console.log('onPlay')
this.$emit('play', player)
},
onPause: (player) => {
this.loading = false
console.log('onPause')
this.$emit('pause', player)
},
onEnded: (player) => {
console.log('onEnded')
this.$emit('ended', player)
},
onStalled: (player) => {
console.log('onStalled')
this.$emit('stalled', player)
},
onSourceEstablished: (source) => {
console.log('onSourceEstablished')
this.flags.noSignal = false
this.loading = false
clearTimeout(this.timers.noSignal)
this.timers.noSignal = null
this.$emit('source-established', source)
},
onSourceCompleted: (source) => {
console.log('onSourceCompleted')
this.$emit('source-completed', source)
},
onSourceConnected: () => {
console.log('onSourceConnected')
clearTimeout(this.timers.noSignal)
this.loading = true
this.flags.noSignal = false
this.$emit('source-connected')
},
onSourceStreamInterrupt: () => {
console.log('onSourceStreamInterrupt')
this.loading = true
clearTimeout(this.timers.noSignal)
this.timers.noSignal = setTimeout(this.handleNoSignal, 15000)
this.$emit('source-interrupt')
},
onSourceStreamContinue: () => {
console.log('onSourceStreamContinue')
clearTimeout(this.timers.noSignal)
this.timers.noSignal = null
this.loading = false
this.flags.noSignal = false
this.$emit('source-continue')
},
onSourceClosed: () => {
console.log('onSourceClosed')
clearTimeout(this.timers.noSignal)
this.$emit('source-closed')
this.handleNoSignal()
},
onResolutionDecode: () => {
// 从流中获取到视频的分辨率
this.flags.gotResolution = true
this.settingPlayer('autoStretch', this.playerSettings.autoStretch)
this.$emit('resolution-decode')
}
})
this.playerSettings.backgroudPlay = !this.options.pauseWhenHidden
if (this.defaultMute) {
this.volume = 0
}
this.timers.noSignal = setTimeout(this.handleNoSignal, 15000)
for (const key in this.playerSettings) {
this.settingPlayer(key, this.playerSettings[key])
}
console.log('player', this.player)
},
rotate(angle, append = false) {
this.player.rotate(angle, append)
},
/**
* 进入画中画模式
* @deprecated 未实现
*/
requesPip() {
// if (!document.pictureInPictureElement) {
// this.$refs.canvas.requestPictureInPicture()
// }
},
/**
* 退出画中画模式
* @deprecated 未实现
*/
exitPip() {
// document.exitPictureInPicture()
},
/**
* 切换全屏模式
*/
toggleFullscreen() {
if (this.isFullscreen) {
fullscreen.exit(this.$el)
} else {
fullscreen.request(this.$el, () => {
this.isFullscreen = false
})
}
this.isFullscreen = !this.isFullscreen
},
play() {
if (!this.url) return
this.loading = true
if (!this.player) {
this.initPlayer()
}
this.player?.play()
},
/**
* 切换播放模式
*/
togglePlay() {
if (this.paused) {
this.play()
} else {
this.pause()
}
},
pause() {
this.player?.pause()
},
intoFront() {
this.player?.intoFront()
},
intoBackground() {
this.player?.intoBackground()
},
stop(clear) {
this.player?.stop(clear)
},
nextFrame() {
this.player?.nextFrame()
},
destroyPlayer() {
this.stop()
this.player?.destroy()
this.player = null
},
mute() {
this.lastVolume = this.volume
this.volume = 0
},
toggleMute() {
if (this.isMuted) {
this.volume = this.lastVolume ? this.lastVolume : 1
} else {
this.mute()
}
this.$emit('volume-change', this.volume)
},
/** 截图 */
snapshot() {
this.player?.snapshot(this.title)
},
recording() {
this.player?.recording(this.title)
},
/**
* @param
*/
settingPlayer(optionName, value) {
if (!this.player) return
switch (optionName) {
case 'autoStretch':
if (!this.flags.gotResolution) return
const canvas = this.player.canvas
if (value) {
if (canvas.width > canvas.height) {
canvas.style.width = '100%'
} else {
canvas.style.height = '100%'
}
} else {
canvas.style.width = ''
canvas.style.height = ''
}
break
default:
this.player?.setOption(optionName, value)
break
}
},
handleToolbarBtnClick(cmd) {
if (!this.player) return
switch (cmd) {
case 'play':
this.togglePlay()
break
case 'stop':
this.stop()
break
case 'mute':
this.toggleMute()
break
case 'snapshot':
this.snapshot()
break
case 'recording':
this.recording()
break
case 'fullscreen':
this.toggleFullscreen()
break
}
},
handleNoSignal() {
this.flags.noSignal = true
this.loading = false
this.stop()
this.$emit('no-signal')
},
handlePlayerMouseEnter() {
this.isPlayerHover = true
},
handleCanvasMouseMove() {
this.isPlayerHover = true
clearTimeout(this.canvasMouseMoveTimer)
this.canvasMouseMoveTimer = setTimeout(() => {
this.isPlayerHover = false
}, 3000)
},
handlePlayerMouseLeave() {
clearTimeout(this.canvasMouseMoveTimer)
this.isPlayerHover = false
},
handleCanvasClick() {},
handleToolbarMouseEnter() {
this.isPlayerHover = true
clearTimeout(this.canvasMouseMoveTimer)
},
handleToolbarMouseLeave() {}
}
}
</script>
<style lang='scss'>
$back-color: rgba(
$color: dimgray,
$alpha: 0.8
);
.jsmpeg-player {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
display: flex;
background-color: #000;
button {
background: none;
border: none;
display: flex;
font-size: inherit;
line-height: inherit;
text-transform: none;
text-decoration: none;
cursor: pointer;
overflow: hidden;
}
.jsmpeg-header {
width: 100%;
height: 40px;
line-height: 40px;
position: absolute;
top: 0;
left: 0;
padding: 0 10px;
background: linear-gradient(#000, transparent);
transform: translateY(-100%);
transition: 0.48s transform ease-in-out;
z-index: 10;
&.is-show {
transform: translateY(0);
.recording-title {
display: inline-flex;
// transition: 0.45s display;
}
}
.jsmpeg-title {
color: #fff;
}
.recording-title {
height: 40px;
display: flex;
font-size: 14px;
color: white;
flex-direction: row;
justify-content: flex-end;
align-items: center;
.icon {
width: 10px;
height: 10px;
background-color: red;
border-radius: 5px;
margin-left: 8px;
margin-right: 6px;
transition: 0.25s background-color ease-in;
&.is-hide {
background-color: transparent;
}
}
}
.close-btn {
color: gray;
transition: 0.28s color;
position: absolute;
top: 0;
right: 5px;
font-size: 18px;
&:hover {
color: #f56c6c;
}
}
}
.jsmpeg-canvas__wrap {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
z-index: 1;
canvas {
max-width: 100%;
max-height: 100%;
// transition: 0.28s transform;
}
.no-signal-text {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
color: white;
position: absolute;
top: 0;
left: 0;
background-color: #000;
}
.el-loading-mask {
background-color: transparent;
}
}
.jsmpeg-toolbar {
width: 100%;
height: 45px;
line-height: 36px;
background: linear-gradient(transparent, #000);
padding: 0 8px;
position: absolute;
bottom: 0px;
left: 0px;
display: flex;
flex-direction: row;
align-items: center;
transform: translateY(100%);
transition: 0.48s transform ease-in-out;
z-index: 10;
&.is-show {
transform: translateY(0);
}
.toolbar-btn-container {
height: 35px;
width: 35px;
line-height: 1;
}
.toolbar-btn {
color: whitesmoke !important;
opacity: 0.8;
transition: 0.28s opacity ease-in-out, 0.28s color;
&:hover {
opacity: 1;
}
img.icon {
object-fit: scale-down;
max-width: 100%;
max-height: 100%;
}
}
> .toolbar-btn {
max-height: 35px;
max-width: 35px;
font-size: 24px;
}
.play-btn {
transition: 0.28s color;
// &:hover {
// color: #409eff !important;
// }
// color: #f56c6c !important;
// &.paused {
// color: #409eff !important;
// }
}
.recording-btn {
&.is-recording {
color: #f56c6c !important;
}
}
.stop-btn {
color: #f56c6c !important;
}
.progress-bar {
flex: 1;
padding: 0 10px;
.current-time {
float: right;
cursor: default;
color: whitesmoke !important;
}
}
}
.overlayers {
width: 0;
height: 0;
}
}
.jsmpeg-popover {
border: none !important;
padding: 16px 8px;
min-width: 0 !important;
background-color: $back-color;
.popper__arrow {
&::after {
border-top-color: $back-color !important;
border-radius: 0;
}
}
&.popover-volume {
.volume-value {
font-size: 12px;
text-align: center;
color: white;
}
.el-slider {
margin-top: 10px;
.el-slider__runway {
background: dimgray;
}
.el-slider__bar {
// background: lightgray;
}
.el-slider__marks-text {
color: white !important;
}
}
}
&.popover-setting {
display: flex;
flex-direction: column;
padding: 8px 0;
.setting-item {
color: whitesmoke;
cursor: pointer;
padding: 8px 15px;
// margin: 0 15px;
transition: 0.28s color;
height: 34px;
display: flex;
flex-direction: row;
align-items: center;
&[highlight]:hover {
color: #409eff;
// background-color: #409eff;
}
& + .setting-item {
// border-top: 1px solid lightgray;
}
.label {
text-align: right;
// flex: 1;
width: 80px;
// font-weight: 700;
}
> .input__wrap,
> .icon {
margin: 0 10px;
max-width: 100px;
}
.input__wrap {
display: flex;
flex-direction: row;
align-items: center;
> * {
background-color: transparent;
color: whitesmoke;
}
}
.el-switch {
width: 30px;
&.is-checked {
.el-switch__core::after {
margin-left: -14px !important;
}
}
.el-switch__core {
height: 16px;
width: 100%;
// height: 15px;
margin: 0;
position: relative;
&::after {
height: 12px;
width: 12px;
}
}
}
}
}
}
</style>
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。