JiangShan-app/pages_player/list/devicePlayer.vue
2025-05-22 16:23:08 +08:00

1299 lines
34 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="container">
<u-loading-page :loading="loading" bg-color="#eef3f7" loadingText="FastBee.cn"></u-loading-page>
<view class="card">
<u-sticky>
<view class="player-wrapper">
<div class="container-shell">
<div id="container" @tap='operate' :ref="'container'+refId"></div>
</div>
<div class="player-display" />
<view class="kBps"
v-if="statisticsAll && (screensStats.abps || screensStats.buf || screensStats.fps || screensStats.ts || screensStats.vbps)">
<text class="iconfont icon-dian"></text>
<text class="li" v-if="screensStats.abps">{{ statisticsAll.abps }}abps</text>
<text class="li" v-if="screensStats.buf">{{ statisticsAll.buf }}buf</text>
<text class="li" v-if="screensStats.fps">{{ statisticsAll.fps }}fps</text>
<text class="li" v-if="screensStats.ts">{{ statisticsAll.ts }}ts</text>
<text class="li" v-if="screensStats.vbps">{{ statisticsAll.vbps }}vbps</text>
</view>
<view class="tabbar" v-if="isTabbar" @tap="tapTbabar">
<view class="title">
<text class="iconfont icon-fanhui"></text>
<view class="texts">{{ title }}</view>
</view>
</view>
<slot></slot>
</view>
<!-- <u-tabs class="tab_wrap" :list="[{ name: '实时视频' }, { name: '录像回放' }]" @click="tabClick"
lineWidth="60"></u-tabs> -->
<view class="video-tab-bar">
<view v-for="(item, index) in tabs" :key="index"
:class="['video-tab-item', tabIndex === index ? 'video-tab-item-active' : '']"
@click="switchTab(index)">
<image :src="icons[index]" :class="['video-tab-icon', isRotated[index] ? 'rotated' : '']" />
<text>{{ item }}</text>
</view>
<view class="video-slider" :style="{ left: `${tabIndex * 50}%` }"></view>
</view>
</u-sticky>
<view class="opt_button_wrap">
<view v-if="tabIndex == 0">
<view class="remote-control">
<!-- 圆环中方向控制按钮,圆环中心添加播放按钮 -->
<view class="container-circle">
<view class="circle">
<view class="button top" @click='handlePtz("up")'>
<u-icon name="arrow-up" color="#b9b9b9" size="22"></u-icon>
</view>
<view class="button right" @click='handlePtz("right")'>
<u-icon name="arrow-right" color="#b9b9b9" size="22"></u-icon>
</view>
<view class="button bottom" @click='handlePtz("down")'>
<u-icon name="arrow-down" color="#b9b9b9" size="22"></u-icon>
</view>
<view class="button left" @click='handlePtz("left")'>
<u-icon name="arrow-left" color="#b9b9b9" size="22"></u-icon>
</view>
<!-- 圆环中心的播放按钮 -->
<view class="button center" @click="operate">
<u-icon name="play-right" color="#b9b9b9" size="30"></u-icon>
</view>
</view>
</view>
<!-- 下方的缩放按钮分布于左右两侧 -->
<view class="bottom-controls">
<view class="play_btn_wrap">
<button class="btn_icon-size" @click="handlePtzScale(1)">
<u-icon name="plus" size="24" color="#b9b9b9"></u-icon>
</button>
</view>
<view class="play_btn_wrap">
<button class="btn_icon-size" @click="handlePtzScale(2)">
<u-icon name="minus" size="24" color="#b9b9b9"></u-icon>
</button>
</view>
</view>
</view>
</view>
<view class="playback_wrap" v-show="tabIndex == 1">
<!-- 页面日期选择按钮 -->
<view class="button-trap">
<!-- 左侧部分 -->
<view class="date_wrap" @click="showcalendar = true">
<uni-icons class="calendar-icon" type="calendar-filled" size="30"></uni-icons>
<u--input readonly :value="playbackDate || '今天'"
customStyle="background:rgba(255, 255, 255,0.1); border:none; padding:6rpx 0; margin-left: 16rpx; width:auto;">
</u--input>
</view>
<!-- 右侧部分 -->
<view class="date_wrap">
<uni-icons class="calendar-icon" type="download-filled" size="30"></uni-icons>
</view>
</view>
<video-progress-bar></video-progress-bar>
<u-calendar :show="showcalendar" :minDate="minDate" :maxDate="maxDate" month-num="12"
:defaultDate="defaultDate" @confirm="confirmCalendar" @close="closeCalendar"></u-calendar>
</view>
</view>
</view>
<!-- 底部 Tab 栏 -->
<!-- 实时视频部分导航功能 -->
<view class="tab-bar" v-show="tabIndex == 0">
<view class="tab-item" @click="takeScreenshot" :class="{ active: activeTab === 'screenshot' }">
<view class="tab-icon-wrapper small-icon">
<image src="/static/home/video/screenshot.png" mode="widthFix" class="tab-icon" />
</view>
<text class="tab-text">{{$tt('player.screenshot')}}</text>
</view>
<view class="tab-item" @click="startRecording" :class="{ active: activeTab === 'record' }">
<view class="tab-icon-wrapper small-icon">
<image v-show="startrecord" mode="widthFix" src="/static/home/video/record.png" class="tab-icon" />
<image v-show="!startrecord" mode="widthFix" src="/static/home/video/record.png" class="tab-icon" />
</view>
<text v-show="startrecord" class="tab-text">{{$tt('player.startRecord')}}</text>
<text v-show="!startrecord" class="tab-text">{{$tt('player.stopRecord')}}</text>
</view>
<!-- 中间凸起的对讲按钮 -->
<view class="tab-item tab-item-center" @touchstart="startIntercom" @touchend="endIntercom"
:class="{ active: intercomActive }">
<view class="tab-icon-wrapper tab-icon-center">
<image v-show="broadcastStatus === -1" src="/static/home/video/startintercom.png" mode="widthFix"
class="tab-icon" />
<image v-show="broadcastStatus === 0" src="/static/home/video/loading.png" mode="widthFix"
class="tab-icon" />
<image v-show="broadcastStatus === 1" src="/static/home/video/intercom.png" mode="widthFix"
class="tab-icon" />
</view>
</view>
<view class="tab-item" @click="controlclarity()" :class="{ active: activeTab === 'clarity' }">
<view class="tab-icon-wrapper small-icon">
<image v-if="clarity==='流畅'" src="/static/home/video/lc.png" mode="widthFix" class="tab-icon" />
<image v-if="clarity==='标清'" src="/static/home/video/bq.png" mode="widthFix" class="tab-icon" />
<image v-if="clarity==='高清'" src="/static/home/video/gq.png" mode="widthFix" class="tab-icon" />
</view>
<text class="tab-text">{{$tt('player.definition')}}</text>
</view>
<view class="tab-item" @click="more" :class="{ active: activeTab === 'more' }">
<view class="tab-icon-wrapper small-icon">
<image src="/static/home/video/playlist.png" mode="widthFix" class="tab-icon" />
</view>
<text class="tab-text">{{$tt('player.more')}}</text>
</view>
</view>
<u-action-sheet @select="selectClick" round="3%" :closeOnClickOverlay='false' :actions="claritylist"
:cancelText="$tt('common.cancel')" @close="clarityshow = false" :show="clarityshow"></u-action-sheet>
<!-- 录像部分 -->
<view class="tab-bar" v-if="tabIndex == 1">
<view class="tab-item" @click="screenShot" :class="{ active: activeTab === 'screenshot' }">
<view class="tab-icon-wrapper small-icon">
<image src="/static/home/video/screenshot.png" class="tab-icon" />
</view>
<text class="tab-text">{{$tt('player.screenshot')}}</text>
</view>
<view class="tab-item" @click="operate" :class="{ active: activeTab === 'operate' }">
<view class="tab-icon-wrapper small-icon">
<image src="/static/home/video/play.png" class="tab-icon" />
</view>
<text class="tab-text">{{$tt('player.play')}}</text>
</view>
<view class="tab-item" @click="startRecording" :class="{ active: activeTab === 'record' }">
<view class="tab-icon-wrapper small-icon">
<image src="/static/home/video/record.png" class="tab-icon" />
</view>
<text class="tab-text">{{$tt('player.Recording')}}</text>
</view>
</view>
</view>
</template>
<script>
import VideoProgressBar from './VideoProgressBar.vue';
import props from './props.js';
import {
ptzdirection,
ptzscale
} from '@/apis/modules/sip';
import {
startPlay,
playback,
closeStream,
playbackSeek,
playbackSpeed,
getDevRecord
} from '@/apis/modules/player';
import {
getPushUrl,
startBroadcast,
stopBroadcast
} from "@/apis/modules/player";
export default {
name: 'devicePlayer',
mixins: [props],
components: {
VideoProgressBar
},
onLoad(option) {
if (option.deviceId) {
console.log(option, '携带参数')
this.deviceId = option.deviceId
this.channelId = option.channelSipId
}
},
data() {
return {
//录制状态
startrecord: true,
broadcastRtc: null,
//对讲状态
broadcastStatus: -1, // -1 默认状态 0 等待接通 1 接通成功
//清晰度展示
clarityshow: false,
clarity: '流畅',
claritylist: [{
name: '高清',
value: 0
}, {
name: '标清',
value: 1
},
{
name: '流畅',
value: 2
}
],
tabs: ['实时视频', '录像回放'],
icons: [
'../../static/home/video/play.png', // 自定义的图标1
'../../static/home/video/record.png' // 自定义的图标2
],
tabIndex: 0,
isRotated: [false, false], // 初始化旋转状态
intercomActive: false, // 对讲功能激活状态
activeTab: null, // 当前激活的Tab项
// 设备升级模态窗
show: false,
showcalendar: false,
// 加载图标
loading: false,
// 控制模块标题
title: ' 设备通道 ',
// 选项卡索引
//tabIndex: 0,
queryParams: {
deviceSipId: '', //设备sipid
},
// 通道集合
selectChannel: null,
// 设备信息
deviceId: '',
channelId: '',
streamId: '',
streaminfo: {
ssrc: '',
playurl: ''
},
jessibuca: null,
operateBtns: {
fullscreen: true,
play: true,
ptz: true,
zoom: true,
//record: true,
//audio: true,
//scale: true,
},
statisticsAll: null,
playing: false,
quieting: true,
pausing: false,
queryDate: '',
retryCount: 0,
videoVod: false,
vodData: {},
minDate: "",
maxDate: "",
defaultDate: "",
hisData: [],
playbackDate: '',
}
},
mounted() {
window.onerror = (msg) => (this.err = msg);
this.create();
},
methods: {
takeScreenshot() {
this.activeTab = 'screenshot';
if (this.playing) {
const timestamp = new Date().getTime();
const filename = this.deviceId + this.channelId + '-' + timestamp;
this.jessibuca.screenshot(filename, "png", 0.92);
console.log('截图');
}
setTimeout(() => {
this.activeTab = null;
}, 300); // 稍微延长动画时间
},
startRecording() {
if (this.playing) {
if (this.startrecord) {
const timestamp = new Date().getTime();
const filename = this.deviceId + this.channelId + '-' + timestamp;
this.jessibuca.startRecord(filename);
console.log('录制开始');
} else {
this.jessibuca.stopRecordAndSave();
console.log('录制结束')
}
this.startrecord = !this.startrecord;
}
this.activeTab = 'record';
setTimeout(() => {
this.activeTab = null;
}, 300);
},
controlclarity() {
this.clarityshow = true
this.activeTab = 'clarity';
console.log('清晰度控制');
setTimeout(() => {
this.activeTab = null;
}, 300);
},
//清晰度选择
selectClick(index) {
console.log(index)
if (index.value == '0') {
this.clarity = '高清'
}
if (index.value == '1') {
this.clarity = '标清'
}
if (index.value == '2') {
this.clarity = '流畅'
}
},
more() {
this.activeTab = 'more';
uni.showToast({
title: "更多功能尽情期待!",
icon: 'none'
})
setTimeout(() => {
this.activeTab = null;
}, 300);
},
create() {
const jessibuca = new window.JessibucaPro(
Object.assign({
container: this.$refs['container' + this.refId],
videoBuffer: Number(0.2),
decoder: this.decoder + 'pages_player/static/h5/js/jessibuca-pro/decoder-pro.js',
timeout: 20,
debug: false,
debugLevel: 'debug',
isResize: false,
useWCS: false,
useMSE: false,
useSIMD: true,
wcsUseVideoRender: false,
loadingText: '加载中...',
hasAudio: true,
isFlv: true,
showBandwidth: true,
supportDblclickFullscreen: true,
operateBtns: this.operateBtns,
ptzClickType: 'mouseDownAndUp',
forceNoOffscreen: true,
playbackForwardMaxRateDecodeIFrame: 4,
playbackCurrentTimeMove: false,
networkDelayTimeoutReplay: true,
qualityConfig: ['流畅', '标清', '高清'],
defaultStreamQuality: this.clarity,
},
this.options
)
);
this.jessibuca = jessibuca
const _this = this
jessibuca.on('error', function(error) {
console.log('error')
console.log(error)
_this.destroy()
})
jessibuca.on('timeout', function() {
console.log('timeout')
if (_this.retryCount <= 2) {
_this.replay()
_this.retryCount++
}
})
let pre = 0
let cur = 0
jessibuca.on('timeUpdate', function(ts) {
cur = parseInt(ts / 60000)
if (pre !== cur) {
pre++
}
})
if (this.videoVod) {
jessibuca.on('playbackSeek', (timeObj) => {
console.log('playbackSeek!')
_this.seekPlay(timeObj)
})
}
jessibuca.on(JessibucaPro.EVENTS.play, function(play) {
console.log('play success!')
_this.playing = true
})
jessibuca.on(JessibucaPro.EVENTS.pause, function(pause) {
console.log('pause success!')
console.log(pause)
_this.playing = false
})
// jessibuca.on('stats', function (s) {
// console.log('stats is', s)
// })
jessibuca.on(JessibucaPro.EVENTS.ptz, (arrow) => {
console.log('ptz arrow', arrow);
_this.handlePtz(arrow);
})
jessibuca.on(JessibucaPro.EVENTS.crashLog, (value) => {
console.error('crashLog: ', value);
_this.$u.toast({
type: 'error',
message: JSON.stringify(value)
})
})
jessibuca.on(JessibucaPro.EVENTS.close, () => {
!this.isDebug && console.log('jessibuca close');
setTimeout(() => {
_this.cleanPlayer(true);
}, 10)
})
},
initUrl(data) {
if (data) {
this.streamId = data.streamId
this.streaminfo.ssrc = data.ssrc
this.streaminfo.playurl = data.playurl
} else {
this.streamId = ''
this.streaminfo.ssrc = ''
this.streaminfo.playurl = ''
}
},
cleanPlayer(stop) {
this.destroy()
if (stop) {
this.stopPlay()
}
this.playing = false
},
destroy() {
if (this.jessibuca) {
this.jessibuca.destroy()
}
switch (this.tabIndex) {
case 0:
this.videoVod = false;
this.operateBtns.ptz = true;
this.operateBtns.zoom = true;
break;
case 1:
this.videoVod = true;
this.operateBtns.ptz = false;
this.operateBtns.zoom = false;
break;
}
// this.create();
},
// 单击选显卡
switchTab(index) {
//console.log(item)
this.tabIndex = index;
this.$set(this.isRotated, index, !this.isRotated[index]);
if (this.playing) {
this.cleanPlayer(true);
}
switch (this.tabIndex) {
case 0:
this.sendDevicePush();
break;
case 1:
this.initCalendar();
break;
}
},
//直播控制
sendDevicePush: function() {
console.log("通知设备推流1" + this.deviceId + " : " + this.channelId);
if (this.channelId) {
startPlay(this.deviceId, this.channelId).then((response) => {
console.log("开始播放:" + this.deviceId + " : " + this.channelId);
let res = response.data;
console.log("playurl" + res.playurl);
this.streamId = res.streamId;
this.streaminfo.playurl = res.playurl;
this.play();
});
}
},
play: function() {
this.playing = true;
this.jessibuca.play(this.streaminfo.playurl);
},
operate() {
//这是增加动画效果的
this.activeTab = 'operate';
console.log('播放暂停');
setTimeout(() => {
this.activeTab = null;
}, 300);
if (!this.videoVod) {
if (this.streaminfo.playurl) {
if (this.playing) {
this.jessibuca.pause();
this.pausing = true;
} else {
this.play();
this.pausing = false;
}
} else {
this.sendDevicePush();
}
} else {
if (this.streaminfo.ssrc && this.playing) {
this.pausing = true
this.playing = false
this.jessibuca.playbackPause()
} else {
this.pausing = false
this.playing = true
this.jessibuca.playbackResume()
}
}
},
stopPlay() {
if (this.streamId && this.playing) {
closeStream(this.deviceId, this.channelId, this.streamId).then(res => {
this.pausing = false
this.playing = false
if (this.jessibuca) {
this.retryCount = 0
this.destroy()
}
this.streamId = '';
this.streaminfo = {
ssrc: '',
playurl: ''
};
})
}
},
handlePtz(arrow) {
let leftRight = 0;
let upDown = 0;
if (arrow === 'left') {
leftRight = 2;
} else if (arrow === 'right') {
leftRight = 1;
} else if (arrow === 'up') {
upDown = 1;
} else if (arrow === 'down') {
upDown = 2;
}
var data = {
leftRight: leftRight,
upDown: upDown,
moveSpeed: 125
};
ptzdirection(this.deviceId, this.channelId, data).then(async response => {
//console.log("云台方向控制:" + JSON.stringify(response));
});
},
handlePtzScale(inOut) {
console.log('云台缩放:' + inOut);
var data = {
inOut: inOut,
scaleSpeed: 30
}
ptzscale(this.deviceId, this.channelId, data);
// 放大/缩小后自动关闭
setTimeout(() => {
ptzscale(this.deviceId, this.channelId, {
inOut: 0,
scaleSpeed: 30
})
}, 800);
},
//语音对讲
// 用户长按开始对讲
startIntercom() {
this.intercomActive = true;
console.log('对讲开始');
this.broadcastStatusClick();
},
// 用户松开结束对讲
endIntercom() {
this.broadcastStatus = -1
this.intercomActive = false;
console.log('对讲结束');
// 这里可以添加对讲结束的逻辑,如停止录音
},
broadcastStatusClick() {
if (this.broadcastStatus == -1) {
// 默认状态, 开始
this.broadcastStatus = 0
// 发起语音对讲
getPushUrl(this.deviceId, this.channelId).then((res) => {
console.log(res)
if (res.code === 200) {
let streamInfo = res.data;
if (document.location.protocol.includes("https")) {
this.startwebrtc(streamInfo.rtcs)
} else {
this.startwebrtc(streamInfo.rtc)
}
} else {
console.log('获取对讲推流地址失败!')
}
});
} else if (this.broadcastStatus === 1) {
this.broadcastStatus = -1;
this.broadcastRtc.close()
}
},
startwebrtc(url) {
// 获取推流鉴权Key
this.broadcastRtc = new ZLMRTCClient.Endpoint({
debug: true, // 是否打印日志
zlmsdpUrl: url, //流地址
simulecast: false,
useCamera: false,
audioEnable: true,
videoEnable: false,
recvOnly: false,
})
this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_NOT_SUPPORT, (e) => { // 获取到了本地流
console.error('不支持webrtc', e)
this.broadcastStatus = -1;
});
this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_ICE_CANDIDATE_ERROR, (e) => { // ICE 协商出错
console.error('ICE 协商出错')
this.broadcastStatus = -1;
});
this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_OFFER_ANWSER_EXCHANGE_FAILED, (e) => { // offer anwser 交换失败
console.error('offer anwser 交换失败', e)
this.broadcastStatus = -1;
});
this.broadcastRtc.on(ZLMRTCClient.Events.WEBRTC_ON_CONNECTION_STATE_CHANGE, (e) => { // offer anwser 交换失败
console.log('状态改变', e)
if (e === "connecting") {
this.broadcastStatus = 0;
} else if (e === "connected") {
this.broadcastStatus = 1;
startBroadcast(this.deviceId, this.channelId).then((res) => {
if (res.code === 200) {
console.log("开始推流!");
}
})
} else if (e === "disconnected") {
this.broadcastStatus = -1;
}
});
this.broadcastRtc.on(ZLMRTCClient.Events.CAPTURE_STREAM_FAILED, (e) => { // offer anwser 交换失败
console.log('捕获流失败', e)
this.broadcastStatus = -1;
});
},
stopBroadcast() {
this.broadcastRtc.close();
this.broadcastStatus = -1;
stopBroadcast(this.deviceId, this.channelId).then((res) => {
if (res.code == 200) {
this.broadcastStatus = -1;
this.broadcastRtc.close();
} else {
console.log('结束对讲失败!')
}
});
},
//录像控制
initCalendar() {
this.defaultDate = new Date();
let minstamp = new Date().getTime() - 1000 * 3600 * 24 * 28 * 12;
let maxstamp = new Date().getTime();
var mindate = new Date(minstamp);
let minmonth = mindate.getMonth() + 1;
let minday = mindate.getDate();
if (minmonth <= 9) {
minmonth = '0' + minmonth;
}
if (minday <= 9) {
minday = '0' + minday;
}
var maxdate = new Date(maxstamp);
let maxmonth = maxdate.getMonth() + 1;
let maxday = maxdate.getDate();
if (maxmonth <= 9) {
maxmonth = '0' + maxmonth;
}
if (maxday <= 9) {
maxday = '0' + maxday;
}
this.minDate = mindate.getFullYear() + '-' + minmonth + '-' + minday;
this.maxDate = maxdate.getFullYear() + '-' + maxmonth + '-' + maxday;
},
confirmCalendar(n) {
this.playbackDate = n[0];
this.showcalendar = false;
this.queryDate = n;
if (!this.jessibuca) {
this.create();
}
this.loadDevRecord();
},
closeCalendar(n) {
this.showcalendar = false;
},
loadDevRecord() {
this.cleanPlayer(false);
if (this.deviceId && this.channelId) {
const date = this.queryDate ? new Date(new Date(this.queryDate).toLocaleDateString()).getTime() :
new Date(new Date().toLocaleDateString()).getTime()
const start = date / 1000
const end = Math.floor((date + 24 * 60 * 60 * 1000 - 1) / 1000)
const query = {
start: start,
end: end,
}
getDevRecord(this.deviceId, this.channelId, query).then(async (response) => {
if (response.data != null && response.data.recordItems != null) {
this.hisData = response.data.recordItems
const len = this.hisData.length
if (len > 0) {
if (this.hisData[0].start < start) {
this.vodData = {
start: start,
end: end,
base: start
}
this.hisData[0].start = start
} else {
this.vodData = {
start: this.hisData[0].start,
end: end,
base: start
}
}
this.playback(this.hisData)
} else {
this.$u.toast('当前通道没有录像');
}
} else {
this.$u.toast('当前通道没有录像');
}
})
}
},
triggerPlay(playTimes) {
if (this.playing) {
this.jessibuca.on('play', () => {
this.quieting = this.jessibuca.quieting
})
if (this.videoVod) {
this.jessibuca.playback(this.streaminfo.playurl, {
playList: playTimes,
fps: 20, //FPS定频(本地设置)生效)
showControl: true,
showRateBtn: true,
isUseFpsRender: true, // 是否使用固定的fps渲染如果设置的fps小于流推过来的会造成内存堆积甚至溢出
isCacheBeforeDecodeForFpsRender: false, // rfs渲染时是否在解码前缓存数据
supportWheel: true, // 是否支持滚动轴切换精度。
rateConfig: [{
label: '正常',
value: 1
},
{
label: '2倍',
value: 2
},
{
label: '4倍',
value: 4
},
{
label: '8倍',
value: 8
},
],
})
}
}
},
playback(playTimes) {
if (this.deviceId && this.channelId) {
this.pausing = false
if (this.streamId) {
closeStream(this.deviceId, this.channelId, this.streamId).then(res => {
const query = {
start: this.vodData.start,
end: this.vodData.end,
}
playback(this.deviceId, this.channelId, query).then(res => {
this.playing = true
console.log(res.data);
this.initUrl(res.data)
}).finally(() => {
this.triggerPlay(playTimes)
})
})
} else {
const query = {
start: this.vodData.start,
end: this.vodData.end,
}
playback(this.deviceId, this.channelId, query).then(res => {
console.log(res)
this.playing = true
this.initUrl(res.data)
}).finally(() => {
this.triggerPlay(playTimes)
})
}
}
},
scalePlay() {
if (this.streaminfo.ssrc && this.playing) {
playbackSpeed(this.deviceId, this.channelId, this.streamId, this.speed).then(res => {
//this.jessibuca.scale(this.speed)
})
}
},
seekPlay(s) {
const curTime = this.vodData.base + s.hour * 3600 + s.min * 60 + s.second
const seekRange = curTime - this.vodData.start
if (this.streaminfo.ssrc && this.playing) {
const query = {
seek: seekRange,
}
playbackSeek(this.deviceId, this.channelId, this.streamId, query).then(res => {
this.jessibuca.setPlaybackStartTime(curTime)
})
}
},
screenShot() {
this.activeTab = 'screenshot';
console.log('截图');
setTimeout(() => {
this.activeTab = null;
}, 300); // 稍微延长动画时间
if (this.playing) {
this.jessibuca.screenshot()
}
},
replay() {
this.cleanPlayer(false);
if (this.videoVod) {
this.playback(this.hisData);
} else {
this.play();
}
},
changeSpeed() {
if (this.speed !== val) {
this.speed = val
this.scalePlay()
}
}
},
onUnload() {
this.cleanPlayer(true);
}
}
</script>
<style lang="scss">
@import '/pages_player/static/icon/iconfont.css';
.container {
background: radial-gradient(circle, #e9faff, #f5f5f5); // 整体背景颜色变得更亮
position: relative;
padding-bottom: 150rpx;
/* 给底部 Tab 栏留出空间 */
}
.player-wrapper {
display: flex;
place-content: center;
font-size: 12px;
width: 100%;
height: 260px;
.player-display {
position: absolute;
left: 4px;
bottom: 48px;
display: flex;
width: 140px;
height: 30px;
justify-content: center;
color: #fff;
}
.container-shell {
position: relative;
width: 100%;
}
#container {
background: rgba(13, 14, 27, 1);
width: 100%;
height: 100%;
position: relative;
}
}
// 中部切换录像与直播tab标签
.video-tab-bar {
display: flex;
position: relative;
justify-content: space-around;
background: rgba(244, 246, 246, 1);
padding: 10rpx 0;
border-bottom: #767676 solid 1rpx;
//box-shadow: 0 0 25rpx rgba(0, 0, 0, 0.25);
}
.video-tab-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 0;
font-size: 30rpx;
font-weight: 500;
transition: all 0.3s ease;
&:hover {
transform: scale(1.1); // 鼠标悬浮时放大
}
&.video-tab-item-active {
color: #3378FE; // 激活状态使用更深的蓝色
// text-shadow: 0 3rpx 5rpx rgba(2, 119, 189, 0.6);
}
.video-tab-icon {
margin-right: 12rpx;
width: 40rpx;
height: 40rpx;
transition: transform 0.4s ease;
}
.video-tab-icon.rotated {
transform: rotate(360deg); // 激活时旋转
}
}
.video-slider {
position: absolute;
bottom: 0;
margin-left: 12.5%;
width: 25%;
height: 7rpx;
background: linear-gradient(90deg, #3378FE, #486ff2); // 渐变浅蓝到深蓝色
border-radius: 4rpx;
box-shadow: 0 3rpx 8rpx rgba(2, 136, 209, 0.5);
transition: left 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55); // 添加弹跳效果
}
.opt_button_wrap {
margin-top: 40px;
.live_str_wrap {
display: flex;
flex-direction: row;
}
.live_str_item {
display: flex;
flex-direction: row;
margin-top: 60px;
}
.remote-control {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.play_btn_wrap {
margin: 10rpx;
button {
width: 120rpx;
height: 80rpx;
background: linear-gradient(145deg, #ffffff, #d8e8f6);
border-radius: 40rpx;
box-shadow: 6rpx 6rpx 12rpx rgba(0, 0, 0, 0.3), -6rpx -6rpx 12rpx rgba(255, 255, 255, 0.9); // 更柔和的阴影效果
display: flex;
align-items: center;
justify-content: center;
&:active {
box-shadow: inset 6rpx 6rpx 12rpx rgba(0, 0, 0, 0.1), inset -6rpx -6rpx 12rpx rgba(255, 255, 255, 0.7); // 按下效果更深一些
}
}
}
.container-circle {
margin: 50rpx;
position: relative;
width: 375rpx;
height: 375rpx;
.circle {
position: relative;
width: 100%;
height: 100%;
border-radius: 50%;
background: radial-gradient(circle, #d8e8f6, #fff);
box-shadow: 6rpx 6rpx 12rpx rgba(0, 0, 0, 0.3), -6rpx -6rpx 12rpx rgba(255, 255, 255, 0.9); // 外圈阴影调整
&:before {
content: '';
position: absolute;
width: 175rpx;
height: 175rpx;
//background: radial-gradient(circle, #d0e6f6, #d0e6f6); // 中间渐变效果
border-radius: 50%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
box-shadow: inset 4rpx 4rpx 8rpx rgba(0, 0, 0, 0.1), inset -4rpx -4rpx 8rpx rgba(255, 255, 255, 0.7); // 内圈拟态效果
}
.button {
width: 110rpx;
height: 110rpx;
background-color: rgba(221, 239, 251, 0.1); // 更亮的白色背景
border-radius: 50%;
position: absolute;
display: flex;
align-items: center;
justify-content: center;
transition: box-shadow 0.3s;
&:active {
box-shadow: inset 5rpx 5rpx 15rpx rgba(0, 0, 0, 0.1), inset -5rpx -5rpx 15rpx rgba(255, 255, 255, 0.7);
}
}
.top {
top: 0;
left: 50%;
transform: translate(-50%, 0%);
}
.right {
top: 50%;
right: 0;
transform: translate(0%, -50%);
}
.bottom {
bottom: 0;
left: 50%;
transform: translate(-50%, 0%);
}
.left {
top: 50%;
left: 0;
transform: translate(0%, -50%);
}
/* 播放按钮居中 */
.center {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 120rpx;
height: 120rpx;
background: linear-gradient(145deg, #ffffff, #d8e8f6); // 中心按钮渐变背景
box-shadow: 6rpx 6rpx 12rpx rgba(0, 0, 0, 0.3), -6rpx -6rpx 12rpx rgba(255, 255, 255, 0.9); // 中心按钮拟态效果
}
}
}
.bottom-controls {
display: flex;
justify-content: space-around;
width: 80%;
margin-top: 30rpx;
}
.playback_wrap {
padding: 0 20px;
background-color: rgba(244, 246, 246, 0.1);
.button-trap {
margin-top: -30rpx;
display: flex;
justify-content: space-between;
/* 将内容分布在左右两侧 */
align-items: center;
/* 垂直居中对齐 */
width: 100%;
/* 容器宽度为 100% */
padding: 20rpx;
/* 外边距,适配不同屏幕 */
.date_wrap {
display: flex;
align-items: center;
background-color: rgba(248, 248, 248, 0.8);
padding: 12rpx 20rpx;
border-radius: 20rpx;
cursor: pointer;
.calendar-icon {
margin-right: 12rpx;
/* 图标与输入框的间距 */
color: #606266;
}
u--input {
flex: 1;
font-size: 28rpx;
/* 输入框字体大小 */
color: #000;
}
}
}
.button_wrap {
display: flex;
flex-direction: row;
margin-top: 56px;
}
}
}
/* Tab栏整体 */
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 130rpx;
background: radial-gradient(circle, #d0e6f6, #f0faff);
/* 浅蓝色渐变背景 */
box-shadow: 0 0 25rpx rgba(0, 0, 0, 0.25);
/* 强化阴影 */
display: flex;
justify-content: space-around;
align-items: flex-end;
border-top-left-radius: 50rpx;
border-top-right-radius: 50rpx;
padding-bottom: 10rpx;
transition: background 0.5s ease;
z-index: 999
}
/* 单个Tab项 */
.tab-item {
flex: 1;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
transition: all 0.4s ease;
}
/* 中间凸起的对讲按钮 */
.tab-item-center {
position: relative;
width: 130rpx;
height: 120rpx;
margin-bottom: 35rpx;
background-color: #d8ebf8;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
transition: all 0.4s ease;
z-index: 1;
/* 中间对讲按钮图标 */
.tab-icon-center {
width: 80rpx;
height: 80rpx;
background: radial-gradient(circle, #42a7ff, #3cabff, #9ed8f6, #cbf0ff);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 10rpx 20rpx rgba(0, 0, 0, 0.2);
.tab-icon {
width: 55rpx;
}
}
}
/* Tab激活时的状态 */
.tab-item.active .small-icon {
transform: translateY(0rpx) scale(1.1);
/* 激活时上浮并放大 */
box-shadow: 0 10rpx 20rpx rgba(0, 0, 0, 0.2);
/* 动态阴影 */
}
/* 图标的容器 */
.tab-icon-wrapper {
width: 60rpx;
height: 60rpx;
background-color: #f0faff;
/* 与背景保持一致的浅蓝色 */
border-radius: 50%;
padding: 10rpx;
display: flex;
justify-content: center;
align-items: center;
transition: all 0.4s ease;
}
/* 图标 */
.tab-icon {
width: 40rpx;
height: 40rpx;
}
/* 文字 */
.tab-text {
margin-top: 10rpx;
font-size: 26rpx;
color: #555;
transition: color 0.4s ease;
}
/* 点击时的效果 */
.tab-item:active .tab-icon-wrapper {
background-color: #a3d7ff;
/* 浅蓝色点击效果背景 */
}
.tab-item:active .tab-text {
color: #1e90ff;
/* 点击时文字变为蓝色 */
}
/* 定义放大缩小的动画 */
@keyframes scaleAnimation {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
/* 点击时触发动画效果,动画无限循环 */
.tab-item-center:active .tab-icon-center {
animation: scaleAnimation 3s ease-in-out infinite;
/* 无限循环 */
box-shadow: 0 15rpx 25rpx rgba(0, 0, 0, 0.3);
}
.tab-item-center:active {
height: 130rpx;
}
</style>