add web
14
.gitignore
vendored
@ -108,3 +108,17 @@ DerivedDataCache/*
|
||||
*.out
|
||||
*.app
|
||||
|
||||
.DS_Store
|
||||
web/node_modules
|
||||
web/dist
|
||||
|
||||
|
||||
# local env files
|
||||
web/.env.local
|
||||
web/.env.*.local
|
||||
|
||||
# Log files
|
||||
web/npm-debug.log*
|
||||
web/yarn-debug.log*
|
||||
web/yarn-error.log*
|
||||
web/pnpm-debug.log*
|
||||
|
5
web/babel.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
30417
web/package-lock.json
generated
Normal file
49
web/package.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "client",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.6.5",
|
||||
"echarts": "^5.3.2",
|
||||
"lib-flexible": "^0.3.2",
|
||||
"vue": "^2.6.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "~4.5.13",
|
||||
"@vue/cli-plugin-eslint": "~4.5.13",
|
||||
"@vue/cli-service": "~4.5.13",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"postcss-px2rem": "^0.3.0",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
},
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"no-undef": "off"
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
}
|
BIN
web/public/favicon.ico
Normal file
After Width: 32px | Height: 32px | Size: 4.2 KiB |
18
web/public/index.html
Normal file
@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<script type="text/javascript" src="./scripts/webRtcPlayer.js"></script>
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
533
web/public/scripts/webRtcPlayer.js
Normal file
@ -0,0 +1,533 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
// universal module definition - read https://www.davidbcalhoun.com/2014/what-is-amd-commonjs-and-umd/
|
||||
|
||||
(function (root, factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define(["./adapter"], factory);
|
||||
} else if (typeof exports === 'object') {
|
||||
// Node. Does not work with strict CommonJS, but
|
||||
// only CommonJS-like environments that support module.exports,
|
||||
// like Node.
|
||||
module.exports = factory(require("./adapter"));
|
||||
} else {
|
||||
// Browser globals (root is window)
|
||||
root.webRtcPlayer = factory(root.adapter);
|
||||
}
|
||||
}(this, function (adapter) {
|
||||
|
||||
function webRtcPlayer(parOptions) {
|
||||
parOptions = typeof parOptions !== 'undefined' ? parOptions : {};
|
||||
|
||||
var self = this;
|
||||
|
||||
//**********************
|
||||
//Config setup
|
||||
//**********************
|
||||
this.cfg = typeof parOptions.peerConnectionOptions !== 'undefined' ? parOptions.peerConnectionOptions : {};
|
||||
this.cfg.sdpSemantics = 'unified-plan';
|
||||
// this.cfg.rtcAudioJitterBufferMaxPackets = 10;
|
||||
// this.cfg.rtcAudioJitterBufferFastAccelerate = true;
|
||||
// this.cfg.rtcAudioJitterBufferMinDelayMs = 0;
|
||||
|
||||
// If this is true in Chrome 89+ SDP is sent that is incompatible with UE Pixel Streaming 4.26 and below.
|
||||
// However 4.27 Pixel Streaming does not need this set to false as it supports `offerExtmapAllowMixed`.
|
||||
// tdlr; uncomment this line for older versions of Pixel Streaming that need Chrome 89+.
|
||||
this.cfg.offerExtmapAllowMixed = false;
|
||||
|
||||
//**********************
|
||||
//Variables
|
||||
//**********************
|
||||
this.pcClient = null;
|
||||
this.dcClient = null;
|
||||
this.tnClient = null;
|
||||
|
||||
this.sdpConstraints = {
|
||||
offerToReceiveAudio: 1, //Note: if you don't need audio you can get improved latency by turning this off.
|
||||
offerToReceiveVideo: 1,
|
||||
voiceActivityDetection: false
|
||||
};
|
||||
|
||||
// See https://www.w3.org/TR/webrtc/#dom-rtcdatachannelinit for values (this is needed for Firefox to be consistent with Chrome.)
|
||||
this.dataChannelOptions = {ordered: true};
|
||||
|
||||
// This is useful if the video/audio needs to autoplay (without user input) as browsers do not allow autoplay non-muted of sound sources without user interaction.
|
||||
this.startVideoMuted = typeof parOptions.startVideoMuted !== 'undefined' ? parOptions.startVideoMuted : false;
|
||||
this.autoPlayAudio = typeof parOptions.autoPlayAudio !== 'undefined' ? parOptions.autoPlayAudio : true;
|
||||
|
||||
// To enable mic in browser use SSL/localhost and have ?useMic in the query string.
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
this.useMic = urlParams.has('useMic');
|
||||
if(!this.useMic)
|
||||
{
|
||||
console.log("Microphone access is not enabled. Pass ?useMic in the url to enable it.");
|
||||
}
|
||||
|
||||
// When ?useMic check for SSL or localhost
|
||||
let isLocalhostConnection = location.hostname === "localhost" || location.hostname === "127.0.0.1";
|
||||
let isHttpsConnection = location.protocol === 'https:';
|
||||
if(this.useMic && !isLocalhostConnection && !isHttpsConnection)
|
||||
{
|
||||
this.useMic = false;
|
||||
console.error("Microphone access in the browser will not work if you are not on HTTPS or localhost. Disabling mic access.");
|
||||
console.error("For testing you can enable HTTP microphone access Chrome by visiting chrome://flags/ and enabling 'unsafely-treat-insecure-origin-as-secure'");
|
||||
}
|
||||
|
||||
// Latency tester
|
||||
this.latencyTestTimings =
|
||||
{
|
||||
TestStartTimeMs: null,
|
||||
UEReceiptTimeMs: null,
|
||||
UEPreCaptureTimeMs: null,
|
||||
UEPostCaptureTimeMs: null,
|
||||
UEPreEncodeTimeMs: null,
|
||||
UEPostEncodeTimeMs: null,
|
||||
UETransmissionTimeMs: null,
|
||||
BrowserReceiptTimeMs: null,
|
||||
FrameDisplayDeltaTimeMs: null,
|
||||
Reset: function()
|
||||
{
|
||||
this.TestStartTimeMs = null;
|
||||
this.UEReceiptTimeMs = null;
|
||||
this.UEPreCaptureTimeMs = null;
|
||||
this.UEPostCaptureTimeMs = null;
|
||||
this.UEPreEncodeTimeMs = null;
|
||||
this.UEPostEncodeTimeMs = null;
|
||||
this.UETransmissionTimeMs = null;
|
||||
this.BrowserReceiptTimeMs = null;
|
||||
this.FrameDisplayDeltaTimeMs = null;
|
||||
},
|
||||
SetUETimings: function(UETimings)
|
||||
{
|
||||
this.UEReceiptTimeMs = UETimings.ReceiptTimeMs;
|
||||
this.UEPreCaptureTimeMs = UETimings.PreCaptureTimeMs;
|
||||
this.UEPostCaptureTimeMs = UETimings.PostCaptureTimeMs;
|
||||
this.UEPreEncodeTimeMs = UETimings.PreEncodeTimeMs;
|
||||
this.UEPostEncodeTimeMs = UETimings.PostEncodeTimeMs;
|
||||
this.UETransmissionTimeMs = UETimings.TransmissionTimeMs;
|
||||
this.BrowserReceiptTimeMs = Date.now();
|
||||
this.OnAllLatencyTimingsReady(this);
|
||||
},
|
||||
SetFrameDisplayDeltaTime: function(DeltaTimeMs)
|
||||
{
|
||||
if(this.FrameDisplayDeltaTimeMs == null)
|
||||
{
|
||||
this.FrameDisplayDeltaTimeMs = Math.round(DeltaTimeMs);
|
||||
this.OnAllLatencyTimingsReady(this);
|
||||
}
|
||||
},
|
||||
OnAllLatencyTimingsReady: function(Timings){}
|
||||
}
|
||||
|
||||
//**********************
|
||||
//Functions
|
||||
//**********************
|
||||
|
||||
//Create Video element and expose that as a parameter
|
||||
this.createWebRtcVideo = function() {
|
||||
var video = document.createElement('video');
|
||||
|
||||
video.id = "streamingVideo";
|
||||
video.playsInline = true;
|
||||
video.disablepictureinpicture = true;
|
||||
video.muted =true;// self.startVideoMuted;
|
||||
|
||||
video.addEventListener('loadedmetadata', function(e){
|
||||
if(self.onVideoInitialised){
|
||||
self.onVideoInitialised();
|
||||
}
|
||||
}, true);
|
||||
|
||||
// Check if request video frame callback is supported
|
||||
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
|
||||
// The API is supported!
|
||||
|
||||
const onVideoFrameReady = (now, metadata) => {
|
||||
|
||||
if(metadata.receiveTime && metadata.expectedDisplayTime)
|
||||
{
|
||||
const receiveToCompositeMs = metadata.presentationTime - metadata.receiveTime;
|
||||
self.aggregatedStats.receiveToCompositeMs = receiveToCompositeMs;
|
||||
}
|
||||
|
||||
|
||||
// Re-register the callback to be notified about the next frame.
|
||||
video.requestVideoFrameCallback(onVideoFrameReady);
|
||||
};
|
||||
|
||||
// Initially register the callback to be notified about the first frame.
|
||||
video.requestVideoFrameCallback(onVideoFrameReady);
|
||||
}
|
||||
|
||||
return video;
|
||||
}
|
||||
|
||||
this.video = this.createWebRtcVideo();
|
||||
|
||||
onsignalingstatechange = function(state) {
|
||||
console.info('signaling state change:', state)
|
||||
};
|
||||
|
||||
oniceconnectionstatechange = function(state) {
|
||||
console.info('ice connection state change:', state)
|
||||
};
|
||||
|
||||
onicegatheringstatechange = function(state) {
|
||||
console.info('ice gathering state change:', state)
|
||||
};
|
||||
|
||||
handleOnTrack = function(e) {
|
||||
console.log('handleOnTrack', e.streams);
|
||||
|
||||
if (e.track)
|
||||
{
|
||||
console.log('Got track - ' + e.track.kind + ' id=' + e.track.id + ' readyState=' + e.track.readyState);
|
||||
}
|
||||
|
||||
if(e.track.kind == "audio")
|
||||
{
|
||||
handleOnAudioTrack(e.streams[0]);
|
||||
return;
|
||||
}
|
||||
else(e.track.kind == "video" && self.video.srcObject !== e.streams[0])
|
||||
{
|
||||
self.video.srcObject = e.streams[0];
|
||||
console.log('Set video source from video track ontrack.');
|
||||
return;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
handleOnAudioTrack = function(audioMediaStream)
|
||||
{
|
||||
// do nothing the video has the same media stream as the audio track we have here (they are linked)
|
||||
if(self.video.srcObject == audioMediaStream)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// video element has some other media stream that is not associated with this audio track
|
||||
else if(self.video.srcObject && self.video.srcObject !== audioMediaStream)
|
||||
{
|
||||
// create a new audio element
|
||||
let audioElem = document.createElement("Audio");
|
||||
audioElem.srcObject = audioMediaStream;
|
||||
|
||||
// there is no way to autoplay audio (even muted), so we defer audio until first click
|
||||
if(!self.autoPlayAudio) {
|
||||
|
||||
let clickToPlayAudio = function() {
|
||||
audioElem.play();
|
||||
self.video.removeEventListener("click", clickToPlayAudio);
|
||||
};
|
||||
|
||||
self.video.addEventListener("click", clickToPlayAudio);
|
||||
}
|
||||
// we assume the user has clicked somewhere on the page and autoplaying audio will work
|
||||
else {
|
||||
// audioElem.play();
|
||||
}
|
||||
console.log('Created new audio element to play seperate audio stream.');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
setupDataChannel = function(pc, label, options) {
|
||||
try {
|
||||
let datachannel = pc.createDataChannel(label, options);
|
||||
console.log(`Created datachannel (${label})`)
|
||||
|
||||
// Inform browser we would like binary data as an ArrayBuffer (FF chooses Blob by default!)
|
||||
datachannel.binaryType = "arraybuffer";
|
||||
|
||||
datachannel.onopen = function (e) {
|
||||
console.log(`data channel (${label}) connect`)
|
||||
if(self.onDataChannelConnected){
|
||||
self.onDataChannelConnected();
|
||||
}
|
||||
}
|
||||
|
||||
datachannel.onclose = function (e) {
|
||||
console.log(`data channel (${label}) closed`)
|
||||
}
|
||||
|
||||
datachannel.onmessage = function (e) {
|
||||
//console.log(`Got message (${label})`, e.data)
|
||||
if (self.onDataChannelMessage)
|
||||
self.onDataChannelMessage(e.data);
|
||||
}
|
||||
|
||||
return datachannel;
|
||||
} catch (e) {
|
||||
console.warn('No data channel', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
onicecandidate = function (e) {
|
||||
console.log('ICE candidate', e)
|
||||
if (e.candidate && e.candidate.candidate) {
|
||||
self.onWebRtcCandidate(e.candidate);
|
||||
}
|
||||
};
|
||||
|
||||
handleCreateOffer = function (pc) {
|
||||
pc.createOffer(self.sdpConstraints).then(function (offer) {
|
||||
|
||||
// Munging is where we modifying the sdp string to set parameters that are not exposed to the browser's WebRTC API
|
||||
mungeSDPOffer(offer);
|
||||
|
||||
// Set our munged SDP on the local peer connection so it is "set" and will be send across
|
||||
pc.setLocalDescription(offer);
|
||||
if (self.onWebRtcOffer) {
|
||||
self.onWebRtcOffer(offer);
|
||||
}
|
||||
},
|
||||
function () { console.warn("Couldn't create offer") });
|
||||
}
|
||||
|
||||
mungeSDPOffer = function (offer) {
|
||||
|
||||
// turn off video-timing sdp sent from browser
|
||||
//offer.sdp = offer.sdp.replace("http://www.webrtc.org/experiments/rtp-hdrext/playout-delay", "");
|
||||
|
||||
// this indicate we support stereo (Chrome needs this)
|
||||
offer.sdp = offer.sdp.replace('useinbandfec=1', 'useinbandfec=1;stereo=1;sprop-maxcapturerate=48000');
|
||||
|
||||
}
|
||||
|
||||
setupPeerConnection = function (pc) {
|
||||
if (pc.SetBitrate)
|
||||
console.log("Hurray! there's RTCPeerConnection.SetBitrate function");
|
||||
|
||||
//Setup peerConnection events
|
||||
pc.onsignalingstatechange = onsignalingstatechange;
|
||||
pc.oniceconnectionstatechange = oniceconnectionstatechange;
|
||||
pc.onicegatheringstatechange = onicegatheringstatechange;
|
||||
|
||||
pc.ontrack = handleOnTrack;
|
||||
pc.onicecandidate = onicecandidate;
|
||||
};
|
||||
|
||||
generateAggregatedStatsFunction = function(){
|
||||
if(!self.aggregatedStats)
|
||||
self.aggregatedStats = {};
|
||||
|
||||
return function(stats){
|
||||
//console.log('Printing Stats');
|
||||
|
||||
let newStat = {};
|
||||
|
||||
stats.forEach(stat => {
|
||||
// console.log(JSON.stringify(stat, undefined, 4));
|
||||
if (stat.type == 'inbound-rtp'
|
||||
&& !stat.isRemote
|
||||
&& (stat.mediaType == 'video' || stat.id.toLowerCase().includes('video'))) {
|
||||
|
||||
newStat.timestamp = stat.timestamp;
|
||||
newStat.bytesReceived = stat.bytesReceived;
|
||||
newStat.framesDecoded = stat.framesDecoded;
|
||||
newStat.packetsLost = stat.packetsLost;
|
||||
newStat.bytesReceivedStart = self.aggregatedStats && self.aggregatedStats.bytesReceivedStart ? self.aggregatedStats.bytesReceivedStart : stat.bytesReceived;
|
||||
newStat.framesDecodedStart = self.aggregatedStats && self.aggregatedStats.framesDecodedStart ? self.aggregatedStats.framesDecodedStart : stat.framesDecoded;
|
||||
newStat.timestampStart = self.aggregatedStats && self.aggregatedStats.timestampStart ? self.aggregatedStats.timestampStart : stat.timestamp;
|
||||
|
||||
if(self.aggregatedStats && self.aggregatedStats.timestamp){
|
||||
if(self.aggregatedStats.bytesReceived){
|
||||
// bitrate = bits received since last time / number of ms since last time
|
||||
//This is automatically in kbits (where k=1000) since time is in ms and stat we want is in seconds (so a '* 1000' then a '/ 1000' would negate each other)
|
||||
newStat.bitrate = 8 * (newStat.bytesReceived - self.aggregatedStats.bytesReceived) / (newStat.timestamp - self.aggregatedStats.timestamp);
|
||||
newStat.bitrate = Math.floor(newStat.bitrate);
|
||||
newStat.lowBitrate = self.aggregatedStats.lowBitrate && self.aggregatedStats.lowBitrate < newStat.bitrate ? self.aggregatedStats.lowBitrate : newStat.bitrate
|
||||
newStat.highBitrate = self.aggregatedStats.highBitrate && self.aggregatedStats.highBitrate > newStat.bitrate ? self.aggregatedStats.highBitrate : newStat.bitrate
|
||||
}
|
||||
|
||||
if(self.aggregatedStats.bytesReceivedStart){
|
||||
newStat.avgBitrate = 8 * (newStat.bytesReceived - self.aggregatedStats.bytesReceivedStart) / (newStat.timestamp - self.aggregatedStats.timestampStart);
|
||||
newStat.avgBitrate = Math.floor(newStat.avgBitrate);
|
||||
}
|
||||
|
||||
if(self.aggregatedStats.framesDecoded){
|
||||
// framerate = frames decoded since last time / number of seconds since last time
|
||||
newStat.framerate = (newStat.framesDecoded - self.aggregatedStats.framesDecoded) / ((newStat.timestamp - self.aggregatedStats.timestamp) / 1000);
|
||||
newStat.framerate = Math.floor(newStat.framerate);
|
||||
newStat.lowFramerate = self.aggregatedStats.lowFramerate && self.aggregatedStats.lowFramerate < newStat.framerate ? self.aggregatedStats.lowFramerate : newStat.framerate
|
||||
newStat.highFramerate = self.aggregatedStats.highFramerate && self.aggregatedStats.highFramerate > newStat.framerate ? self.aggregatedStats.highFramerate : newStat.framerate
|
||||
}
|
||||
|
||||
if(self.aggregatedStats.framesDecodedStart){
|
||||
newStat.avgframerate = (newStat.framesDecoded - self.aggregatedStats.framesDecodedStart) / ((newStat.timestamp - self.aggregatedStats.timestampStart) / 1000);
|
||||
newStat.avgframerate = Math.floor(newStat.avgframerate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Read video track stats
|
||||
if(stat.type == 'track' && (stat.trackIdentifier == 'video_label' || stat.kind == 'video')) {
|
||||
newStat.framesDropped = stat.framesDropped;
|
||||
newStat.framesReceived = stat.framesReceived;
|
||||
newStat.framesDroppedPercentage = stat.framesDropped / stat.framesReceived * 100;
|
||||
newStat.frameHeight = stat.frameHeight;
|
||||
newStat.frameWidth = stat.frameWidth;
|
||||
newStat.frameHeightStart = self.aggregatedStats && self.aggregatedStats.frameHeightStart ? self.aggregatedStats.frameHeightStart : stat.frameHeight;
|
||||
newStat.frameWidthStart = self.aggregatedStats && self.aggregatedStats.frameWidthStart ? self.aggregatedStats.frameWidthStart : stat.frameWidth;
|
||||
}
|
||||
|
||||
if(stat.type =='candidate-pair' && stat.hasOwnProperty('currentRoundTripTime') && stat.currentRoundTripTime != 0){
|
||||
newStat.currentRoundTripTime = stat.currentRoundTripTime;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if(self.aggregatedStats.receiveToCompositeMs)
|
||||
{
|
||||
newStat.receiveToCompositeMs = self.aggregatedStats.receiveToCompositeMs;
|
||||
self.latencyTestTimings.SetFrameDisplayDeltaTime(self.aggregatedStats.receiveToCompositeMs);
|
||||
}
|
||||
|
||||
self.aggregatedStats = newStat;
|
||||
|
||||
if(self.onAggregatedStats)
|
||||
self.onAggregatedStats(newStat)
|
||||
}
|
||||
};
|
||||
|
||||
setupTracksToSendAsync = async function(pc){
|
||||
|
||||
// Setup a transceiver for getting UE video
|
||||
pc.addTransceiver("video", { direction: "recvonly" });
|
||||
|
||||
// Setup a transceiver for sending mic audio to UE and receiving audio from UE
|
||||
if(!self.useMic)
|
||||
{
|
||||
pc.addTransceiver("audio", { direction: "recvonly" });
|
||||
}
|
||||
else
|
||||
{
|
||||
let audioSendOptions = self.useMic ?
|
||||
{
|
||||
autoGainControl: false,
|
||||
channelCount: 1,
|
||||
echoCancellation: false,
|
||||
latency: 0,
|
||||
noiseSuppression: false,
|
||||
sampleRate: 16000,
|
||||
volume: 1.0
|
||||
} : false;
|
||||
|
||||
// Note using mic on android chrome requires SSL or chrome://flags/ "unsafely-treat-insecure-origin-as-secure"
|
||||
const stream = await navigator.mediaDevices.getUserMedia({video: false, audio: audioSendOptions});
|
||||
if(stream)
|
||||
{
|
||||
for (const track of stream.getTracks()) {
|
||||
if(track.kind && track.kind == "audio")
|
||||
{
|
||||
pc.addTransceiver(track, { direction: "sendrecv" });
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
pc.addTransceiver("audio", { direction: "recvonly" });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
//**********************
|
||||
//Public functions
|
||||
//**********************
|
||||
|
||||
this.setVideoEnabled = function(enabled) {
|
||||
self.video.srcObject.getTracks().forEach(track => track.enabled = enabled);
|
||||
}
|
||||
|
||||
this.startLatencyTest = function(onTestStarted) {
|
||||
// Can't start latency test without a video element
|
||||
if(!self.video)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.latencyTestTimings.Reset();
|
||||
self.latencyTestTimings.TestStartTimeMs = Date.now();
|
||||
onTestStarted(self.latencyTestTimings.TestStartTimeMs);
|
||||
}
|
||||
|
||||
//This is called when revceiving new ice candidates individually instead of part of the offer
|
||||
//This is currently not used but would be called externally from this class
|
||||
this.handleCandidateFromServer = function(iceCandidate) {
|
||||
console.log("ICE candidate: ", iceCandidate);
|
||||
let candidate = new RTCIceCandidate(iceCandidate);
|
||||
self.pcClient.addIceCandidate(candidate).then(_=>{
|
||||
console.log('ICE candidate successfully added');
|
||||
});
|
||||
};
|
||||
|
||||
//Called externaly to create an offer for the server
|
||||
this.createOffer = function() {
|
||||
if(self.pcClient){
|
||||
console.log("Closing existing PeerConnection")
|
||||
self.pcClient.close();
|
||||
self.pcClient = null;
|
||||
}
|
||||
self.pcClient = new RTCPeerConnection(self.cfg);
|
||||
|
||||
setupTracksToSendAsync(self.pcClient).finally(function()
|
||||
{
|
||||
setupPeerConnection(self.pcClient);
|
||||
self.dcClient = setupDataChannel(self.pcClient, 'cirrus', self.dataChannelOptions);
|
||||
handleCreateOffer(self.pcClient);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
//Called externaly when an answer is received from the server
|
||||
this.receiveAnswer = function(answer) {
|
||||
console.log('Received answer:');
|
||||
console.log(answer);
|
||||
var answerDesc = new RTCSessionDescription(answer);
|
||||
self.pcClient.setRemoteDescription(answerDesc);
|
||||
|
||||
let receivers = self.pcClient.getReceivers();
|
||||
for(let receiver of receivers)
|
||||
{
|
||||
receiver.playoutDelayHint = 0;
|
||||
}
|
||||
};
|
||||
|
||||
this.close = function(){
|
||||
if(self.pcClient){
|
||||
console.log("Closing existing peerClient")
|
||||
self.pcClient.close();
|
||||
self.pcClient = null;
|
||||
}
|
||||
if(self.aggregateStatsIntervalId)
|
||||
clearInterval(self.aggregateStatsIntervalId);
|
||||
}
|
||||
|
||||
//Sends data across the datachannel
|
||||
this.send = function(data){
|
||||
if(self.dcClient && self.dcClient.readyState == 'open'){
|
||||
//console.log('Sending data on dataconnection', self.dcClient)
|
||||
self.dcClient.send(data);
|
||||
}
|
||||
};
|
||||
|
||||
this.getStats = function(onStats){
|
||||
if(self.pcClient && onStats){
|
||||
self.pcClient.getStats(null).then((stats) => {
|
||||
onStats(stats);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.aggregateStats = function(checkInterval){
|
||||
let calcAggregatedStats = generateAggregatedStatsFunction();
|
||||
let printAggregatedStats = () => { self.getStats(calcAggregatedStats); }
|
||||
self.aggregateStatsIntervalId = setInterval(printAggregatedStats, checkInterval);
|
||||
}
|
||||
};
|
||||
|
||||
return webRtcPlayer;
|
||||
|
||||
}));
|
838
web/public/scripts/wewbRtcSDK.js
Normal file
@ -0,0 +1,838 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
let webRtcPlayerObj = null;
|
||||
|
||||
let is_reconnection = false;
|
||||
let connect_on_load = false;
|
||||
let ws;
|
||||
const WS_OPEN_STATE = 1;
|
||||
|
||||
// TODO: Remove this - workaround because of bug causing UE to crash when switching resolutions too quickly
|
||||
let lastTimeResized = new Date().getTime();
|
||||
let resizeTimeout;
|
||||
|
||||
let onDataChannelConnected;
|
||||
let responseEventListeners = new Map();
|
||||
|
||||
let shouldShowPlayOverlay = true;
|
||||
|
||||
// A hidden input text box which is used only for focusing and opening the
|
||||
// on-screen keyboard.
|
||||
let hiddenInput = undefined;
|
||||
|
||||
let t0 = Date.now();
|
||||
|
||||
function log(str) {
|
||||
console.log(`${Math.floor(Date.now() - t0)}: ` + str);
|
||||
}
|
||||
|
||||
function setOverlay(htmlClass, htmlElement, onClickFunction) {
|
||||
let videoPlayOverlay = document.getElementById('videoPlayOverlay');
|
||||
if (!videoPlayOverlay) {
|
||||
let playerDiv = document.getElementById('player');
|
||||
videoPlayOverlay = document.createElement('div');
|
||||
videoPlayOverlay.id = 'videoPlayOverlay';
|
||||
playerDiv.appendChild(videoPlayOverlay);
|
||||
}
|
||||
|
||||
// Remove existing html child elements so we can add the new one
|
||||
while (videoPlayOverlay.lastChild) {
|
||||
videoPlayOverlay.removeChild(videoPlayOverlay.lastChild);
|
||||
}
|
||||
|
||||
if (htmlElement)
|
||||
videoPlayOverlay.appendChild(htmlElement);
|
||||
|
||||
if (onClickFunction) {
|
||||
videoPlayOverlay.addEventListener('click', function onOverlayClick(event) {
|
||||
onClickFunction(event);
|
||||
videoPlayOverlay.removeEventListener('click', onOverlayClick);
|
||||
});
|
||||
}
|
||||
|
||||
// Remove existing html classes so we can set the new one
|
||||
let cl = videoPlayOverlay.classList;
|
||||
for (let i = cl.length - 1; i >= 0; i--) {
|
||||
cl.remove(cl[i]);
|
||||
}
|
||||
|
||||
videoPlayOverlay.classList.add(htmlClass);
|
||||
}
|
||||
|
||||
|
||||
function showConnectOverlay() {
|
||||
let startText = document.createElement('div');
|
||||
startText.id = 'playButton';
|
||||
startText.innerHTML = 'Click to start';
|
||||
|
||||
setOverlay('clickableState', startText, event => {
|
||||
connect();
|
||||
});
|
||||
}
|
||||
|
||||
function playVideoStream() {
|
||||
if (webRtcPlayerObj && webRtcPlayerObj.video) {
|
||||
|
||||
webRtcPlayerObj.video.play().catch(function(onRejectedReason){
|
||||
console.error(onRejectedReason);
|
||||
console.log("Browser does not support autoplaying video without interaction - to resolve this we are going to show the play button overlay.")
|
||||
shouldShowPlayOverlay = false;
|
||||
});
|
||||
|
||||
requestInitialSettings();
|
||||
requestQualityControl();
|
||||
hideOverlay();
|
||||
} else {
|
||||
console.error("Could not player video stream because webRtcPlayerObj.video was not valid.")
|
||||
}
|
||||
}
|
||||
|
||||
function updateAfkOverlayText() {
|
||||
afk.overlay.innerHTML = '<center>No activity detected<br>Disconnecting in ' + afk.countdown + ' seconds<br>Click to continue<br></center>';
|
||||
}
|
||||
|
||||
function hideOverlay() {
|
||||
setOverlay('hiddenState');
|
||||
}
|
||||
|
||||
function createWebRtcOffer() {
|
||||
if (webRtcPlayerObj) {
|
||||
console.log('Creating offer');
|
||||
webRtcPlayerObj.createOffer();
|
||||
} else {
|
||||
console.log('WebRTC player not setup, cannot create offer');
|
||||
}
|
||||
}
|
||||
|
||||
function sendInputData(data) {
|
||||
if (webRtcPlayerObj) {
|
||||
webRtcPlayerObj.send(data);
|
||||
}
|
||||
}
|
||||
|
||||
function addResponseEventListener(name, listener) {
|
||||
responseEventListeners.set(name, listener);
|
||||
}
|
||||
|
||||
function removeResponseEventListener(name) {
|
||||
responseEventListeners.remove(name);
|
||||
}
|
||||
|
||||
// Must be kept in sync with PixelStreamingProtocol::EToPlayerMsg C++ enum.
|
||||
const ToClientMessageType = {
|
||||
QualityControlOwnership: 0,
|
||||
Response: 1,
|
||||
Command: 2,
|
||||
FreezeFrame: 3,
|
||||
UnfreezeFrame: 4,
|
||||
VideoEncoderAvgQP: 5,
|
||||
LatencyTest: 6,
|
||||
InitialSettings: 7
|
||||
};
|
||||
|
||||
let VideoEncoderQP = "N/A";
|
||||
|
||||
function setupWebRtcPlayer(htmlElement, config) {
|
||||
webRtcPlayerObj = new webRtcPlayer(config);
|
||||
htmlElement.appendChild(webRtcPlayerObj.video);
|
||||
|
||||
webRtcPlayerObj.onWebRtcOffer = function(offer) {
|
||||
if (ws && ws.readyState === WS_OPEN_STATE) {
|
||||
let offerStr = JSON.stringify(offer);
|
||||
console.log(`-> SS: offer:\n${offerStr}`);
|
||||
ws.send(offerStr);
|
||||
}
|
||||
};
|
||||
|
||||
webRtcPlayerObj.onWebRtcCandidate = function(candidate) {
|
||||
if (ws && ws.readyState === WS_OPEN_STATE) {
|
||||
console.log(`-> SS: iceCandidate\n${JSON.stringify(candidate, undefined, 4)}`);
|
||||
ws.send(JSON.stringify({
|
||||
type: 'iceCandidate',
|
||||
candidate: candidate
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
webRtcPlayerObj.onVideoInitialised = function() {
|
||||
if (ws && ws.readyState === WS_OPEN_STATE) {
|
||||
if (shouldShowPlayOverlay) {
|
||||
shouldShowPlayOverlay = false;
|
||||
resizePlayerStyle();
|
||||
}
|
||||
else {
|
||||
resizePlayerStyle();
|
||||
playVideoStream();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
webRtcPlayerObj.onDataChannelConnected = function() {
|
||||
if (ws && ws.readyState === WS_OPEN_STATE) {
|
||||
if (webRtcPlayerObj.video && webRtcPlayerObj.video.srcObject && webRtcPlayerObj.onVideoInitialised) {
|
||||
webRtcPlayerObj.onVideoInitialised();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
registerInputs(webRtcPlayerObj.video);
|
||||
|
||||
createWebRtcOffer();
|
||||
|
||||
return webRtcPlayerObj.video;
|
||||
}
|
||||
|
||||
function onWebRtcAnswer(webRTCData) {
|
||||
webRtcPlayerObj.receiveAnswer(webRTCData);
|
||||
|
||||
webRtcPlayerObj.aggregateStats(1 * 1000 );
|
||||
}
|
||||
|
||||
function onWebRtcIce(iceCandidate) {
|
||||
if (webRtcPlayerObj)
|
||||
webRtcPlayerObj.handleCandidateFromServer(iceCandidate);
|
||||
}
|
||||
|
||||
let styleWidth;
|
||||
let styleHeight;
|
||||
let styleTop;
|
||||
let styleLeft;
|
||||
let styleCursor = 'default';
|
||||
let styleAdditional;
|
||||
|
||||
const ControlSchemeType = {
|
||||
// A mouse can lock inside the WebRTC player so the user can simply move the
|
||||
// mouse to control the orientation of the camera. The user presses the
|
||||
// Escape key to unlock the mouse.
|
||||
LockedMouse: 0,
|
||||
|
||||
// A mouse can hover over the WebRTC player so the user needs to click and
|
||||
// drag to control the orientation of the camera.
|
||||
HoveringMouse: 1
|
||||
};
|
||||
|
||||
let inputOptions = {
|
||||
// The control scheme controls the behaviour of the mouse when it interacts
|
||||
// with the WebRTC player.
|
||||
controlScheme: ControlSchemeType.LockedMouse,
|
||||
|
||||
// Browser keys are those which are typically used by the browser UI. We
|
||||
// usually want to suppress these to allow, for example, UE4 to show shader
|
||||
// complexity with the F5 key without the web page refreshing.
|
||||
suppressBrowserKeys: true,
|
||||
|
||||
// UE4 has a faketouches option which fakes a single finger touch when the
|
||||
// user drags with their mouse. We may perform the reverse; a single finger
|
||||
// touch may be converted into a mouse drag UE4 side. This allows a
|
||||
// non-touch application to be controlled partially via a touch device.
|
||||
fakeMouseWithTouches: false
|
||||
};
|
||||
|
||||
function resizePlayerStyleToFillWindow(playerElement) {
|
||||
let videoElement = playerElement.getElementsByTagName("VIDEO");
|
||||
|
||||
// Fill the player display in window, keeping picture's aspect ratio.
|
||||
let windowAspectRatio = window.innerHeight / window.innerWidth;
|
||||
let playerAspectRatio = playerElement.clientHeight / playerElement.clientWidth;
|
||||
// We want to keep the video ratio correct for the video stream
|
||||
let videoAspectRatio = videoElement.videoHeight / videoElement.videoWidth;
|
||||
if (isNaN(videoAspectRatio)) {
|
||||
//Video is not initialised yet so set playerElement to size of window
|
||||
styleWidth = window.innerWidth;
|
||||
styleHeight = window.innerHeight;
|
||||
styleTop = 0;
|
||||
styleLeft = 0;
|
||||
playerElement.style = "top: " + styleTop + "px; left: " + styleLeft + "px; width: " + styleWidth + "px; height: " + styleHeight + "px; cursor: " + styleCursor + "; " + styleAdditional;
|
||||
} else if (windowAspectRatio < playerAspectRatio) {
|
||||
// Window height is the constraining factor so to keep aspect ratio change width appropriately
|
||||
styleWidth = Math.floor(window.innerHeight / videoAspectRatio);
|
||||
styleHeight = window.innerHeight;
|
||||
styleTop = 0;
|
||||
styleLeft = Math.floor((window.innerWidth - styleWidth) * 0.5);
|
||||
//Video is now 100% of the playerElement, so set the playerElement style
|
||||
playerElement.style = "top: " + styleTop + "px; left: " + styleLeft + "px; width: " + styleWidth + "px; height: " + styleHeight + "px; cursor: " + styleCursor + "; " + styleAdditional;
|
||||
} else {
|
||||
// Window width is the constraining factor so to keep aspect ratio change height appropriately
|
||||
styleWidth = window.innerWidth;
|
||||
styleHeight = Math.floor(window.innerWidth * videoAspectRatio);
|
||||
styleTop = Math.floor((window.innerHeight - styleHeight) * 0.5);
|
||||
styleLeft = 0;
|
||||
//Video is now 100% of the playerElement, so set the playerElement style
|
||||
playerElement.style = "top: " + styleTop + "px; left: " + styleLeft + "px; width: " + styleWidth + "px; height: " + styleHeight + "px; cursor: " + styleCursor + "; " + styleAdditional;
|
||||
}
|
||||
}
|
||||
|
||||
function resizePlayerStyleToActualSize(playerElement) {
|
||||
let videoElement = playerElement.getElementsByTagName("VIDEO");
|
||||
|
||||
if (videoElement.length > 0) {
|
||||
// Display image in its actual size
|
||||
styleWidth = videoElement[0].videoWidth;
|
||||
styleHeight = videoElement[0].videoHeight;
|
||||
let Top = Math.floor((window.innerHeight - styleHeight) * 0.5);
|
||||
let Left = Math.floor((window.innerWidth - styleWidth) * 0.5);
|
||||
styleTop = (Top > 0) ? Top : 0;
|
||||
styleLeft = (Left > 0) ? Left : 0;
|
||||
//Video is now 100% of the playerElement, so set the playerElement style
|
||||
playerElement.style = "top: " + styleTop + "px; left: " + styleLeft + "px; width: " + styleWidth + "px; height: " + styleHeight + "px; cursor: " + styleCursor + "; " + styleAdditional;
|
||||
}
|
||||
}
|
||||
|
||||
function resizePlayerStyle(event) {
|
||||
let playerElement = document.getElementById('player');
|
||||
|
||||
if (!playerElement)
|
||||
return;
|
||||
|
||||
resizePlayerStyleToFillWindow(playerElement);
|
||||
//resizePlayerStyleToActualSize(playerElement);
|
||||
setupMouseAndFreezeFrame(playerElement)
|
||||
}
|
||||
|
||||
function setupMouseAndFreezeFrame(playerElement) {
|
||||
// Calculating and normalizing positions depends on the width and height of
|
||||
// the player.
|
||||
playerElementClientRect = playerElement.getBoundingClientRect();
|
||||
setupNormalizeAndQuantize();
|
||||
}
|
||||
|
||||
// Must be kept in sync with PixelStreamingProtocol::EToUE4Msg C++ enum.
|
||||
const MessageType = {
|
||||
|
||||
/**********************************************************************/
|
||||
|
||||
/*
|
||||
* Control Messages. Range = 0..49.
|
||||
*/
|
||||
IFrameRequest: 0,
|
||||
RequestQualityControl: 1,
|
||||
MaxFpsRequest: 2,
|
||||
AverageBitrateRequest: 3,
|
||||
StartStreaming: 4,
|
||||
StopStreaming: 5,
|
||||
LatencyTest: 6,
|
||||
RequestInitialSettings: 7,
|
||||
|
||||
/**********************************************************************/
|
||||
|
||||
/*
|
||||
* Input Messages. Range = 50..89.
|
||||
*/
|
||||
|
||||
// Generic Input Messages. Range = 50..59.
|
||||
UIInteraction: 50,
|
||||
Command: 51,
|
||||
|
||||
// Keyboard Input Message. Range = 60..69.
|
||||
KeyDown: 60,
|
||||
KeyUp: 61,
|
||||
KeyPress: 62,
|
||||
|
||||
// Mouse Input Messages. Range = 70..79.
|
||||
MouseEnter: 70,
|
||||
MouseLeave: 71,
|
||||
MouseDown: 72,
|
||||
MouseUp: 73,
|
||||
MouseMove: 74,
|
||||
MouseWheel: 75,
|
||||
|
||||
// Touch Input Messages. Range = 80..89.
|
||||
TouchStart: 80,
|
||||
TouchEnd: 81,
|
||||
TouchMove: 82,
|
||||
|
||||
// Gamepad Input Messages. Range = 90..99
|
||||
GamepadButtonPressed: 90,
|
||||
GamepadButtonReleased: 91,
|
||||
GamepadAnalog: 92
|
||||
|
||||
/**************************************************************************/
|
||||
};
|
||||
|
||||
function requestInitialSettings() {
|
||||
sendInputData(new Uint8Array([MessageType.RequestInitialSettings]).buffer);
|
||||
}
|
||||
|
||||
function requestQualityControl() {
|
||||
sendInputData(new Uint8Array([MessageType.RequestQualityControl]).buffer);
|
||||
}
|
||||
|
||||
let playerElementClientRect = undefined;
|
||||
let normalizeAndQuantizeUnsigned = undefined;
|
||||
let normalizeAndQuantizeSigned = undefined;
|
||||
|
||||
function setupNormalizeAndQuantize() {
|
||||
let playerElement = document.getElementById('player');
|
||||
let videoElement = playerElement.getElementsByTagName("video");
|
||||
|
||||
if (playerElement && videoElement.length > 0) {
|
||||
let playerAspectRatio = playerElement.clientHeight / playerElement.clientWidth;
|
||||
let videoAspectRatio = videoElement[0].videoHeight / videoElement[0].videoWidth;
|
||||
|
||||
if (playerAspectRatio > videoAspectRatio) {
|
||||
let ratio = playerAspectRatio / videoAspectRatio;
|
||||
// Unsigned.
|
||||
normalizeAndQuantizeUnsigned = (x, y) => {
|
||||
let normalizedX = x / playerElement.clientWidth;
|
||||
let normalizedY = ratio * (y / playerElement.clientHeight - 0.5) + 0.5;
|
||||
if (normalizedX < 0.0 || normalizedX > 1.0 || normalizedY < 0.0 || normalizedY > 1.0) {
|
||||
return {
|
||||
inRange: false,
|
||||
x: 65535,
|
||||
y: 65535
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
inRange: true,
|
||||
x: normalizedX * 65536,
|
||||
y: normalizedY * 65536
|
||||
};
|
||||
}
|
||||
};
|
||||
unquantizeAndDenormalizeUnsigned = (x, y) => {
|
||||
let normalizedX = x / 65536;
|
||||
let normalizedY = (y / 65536 - 0.5) / ratio + 0.5;
|
||||
return {
|
||||
x: normalizedX * playerElement.clientWidth,
|
||||
y: normalizedY * playerElement.clientHeight
|
||||
};
|
||||
};
|
||||
// Signed.
|
||||
normalizeAndQuantizeSigned = (x, y) => {
|
||||
let normalizedX = x / (0.5 * playerElement.clientWidth);
|
||||
let normalizedY = (ratio * y) / (0.5 * playerElement.clientHeight);
|
||||
return {
|
||||
x: normalizedX * 32767,
|
||||
y: normalizedY * 32767
|
||||
};
|
||||
};
|
||||
} else {
|
||||
let ratio = videoAspectRatio / playerAspectRatio;
|
||||
// Unsigned.
|
||||
normalizeAndQuantizeUnsigned = (x, y) => {
|
||||
let normalizedX = ratio * (x / playerElement.clientWidth - 0.5) + 0.5;
|
||||
let normalizedY = y / playerElement.clientHeight;
|
||||
if (normalizedX < 0.0 || normalizedX > 1.0 || normalizedY < 0.0 || normalizedY > 1.0) {
|
||||
return {
|
||||
inRange: false,
|
||||
x: 65535,
|
||||
y: 65535
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
inRange: true,
|
||||
x: normalizedX * 65536,
|
||||
y: normalizedY * 65536
|
||||
};
|
||||
}
|
||||
};
|
||||
unquantizeAndDenormalizeUnsigned = (x, y) => {
|
||||
let normalizedX = (x / 65536 - 0.5) / ratio + 0.5;
|
||||
let normalizedY = y / 65536;
|
||||
return {
|
||||
x: normalizedX * playerElement.clientWidth,
|
||||
y: normalizedY * playerElement.clientHeight
|
||||
};
|
||||
};
|
||||
// Signed.
|
||||
normalizeAndQuantizeSigned = (x, y) => {
|
||||
let normalizedX = (ratio * x) / (0.5 * playerElement.clientWidth);
|
||||
let normalizedY = y / (0.5 * playerElement.clientHeight);
|
||||
return {
|
||||
x: normalizedX * 32767,
|
||||
y: normalizedY * 32767
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function emitMouseMove(x, y, deltaX, deltaY) {
|
||||
let coord = normalizeAndQuantizeUnsigned(x, y);
|
||||
let delta = normalizeAndQuantizeSigned(deltaX, deltaY);
|
||||
let Data = new DataView(new ArrayBuffer(9));
|
||||
Data.setUint8(0, MessageType.MouseMove);
|
||||
Data.setUint16(1, coord.x, true);
|
||||
Data.setUint16(3, coord.y, true);
|
||||
Data.setInt16(5, delta.x, true);
|
||||
Data.setInt16(7, delta.y, true);
|
||||
sendInputData(Data.buffer);
|
||||
}
|
||||
|
||||
function emitMouseDown(button, x, y) {
|
||||
let coord = normalizeAndQuantizeUnsigned(x, y);
|
||||
let Data = new DataView(new ArrayBuffer(6));
|
||||
Data.setUint8(0, MessageType.MouseDown);
|
||||
Data.setUint8(1, button);
|
||||
Data.setUint16(2, coord.x, true);
|
||||
Data.setUint16(4, coord.y, true);
|
||||
sendInputData(Data.buffer);
|
||||
}
|
||||
|
||||
function emitMouseUp(button, x, y) {
|
||||
let coord = normalizeAndQuantizeUnsigned(x, y);
|
||||
let Data = new DataView(new ArrayBuffer(6));
|
||||
Data.setUint8(0, MessageType.MouseUp);
|
||||
Data.setUint8(1, button);
|
||||
Data.setUint16(2, coord.x, true);
|
||||
Data.setUint16(4, coord.y, true);
|
||||
sendInputData(Data.buffer);
|
||||
}
|
||||
|
||||
function emitMouseWheel(delta, x, y) {
|
||||
let coord = normalizeAndQuantizeUnsigned(x, y);
|
||||
let Data = new DataView(new ArrayBuffer(7));
|
||||
Data.setUint8(0, MessageType.MouseWheel);
|
||||
Data.setInt16(1, delta, true);
|
||||
Data.setUint16(3, coord.x, true);
|
||||
Data.setUint16(5, coord.y, true);
|
||||
sendInputData(Data.buffer);
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
|
||||
const MouseButton = {
|
||||
MainButton: 0, // Left button.
|
||||
AuxiliaryButton: 1, // Wheel button.
|
||||
SecondaryButton: 2, // Right button.
|
||||
FourthButton: 3, // Browser Back button.
|
||||
FifthButton: 4 // Browser Forward button.
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
|
||||
const MouseButtonsMask = {
|
||||
PrimaryButton: 1, // Left button.
|
||||
SecondaryButton: 2, // Right button.
|
||||
AuxiliaryButton: 4, // Wheel button.
|
||||
FourthButton: 8, // Browser Back button.
|
||||
FifthButton: 16 // Browser Forward button.
|
||||
};
|
||||
|
||||
// If the user has any mouse buttons pressed then release them.
|
||||
function releaseMouseButtons(buttons, x, y) {
|
||||
if (buttons & MouseButtonsMask.PrimaryButton) {
|
||||
emitMouseUp(MouseButton.MainButton, x, y);
|
||||
}
|
||||
if (buttons & MouseButtonsMask.SecondaryButton) {
|
||||
emitMouseUp(MouseButton.SecondaryButton, x, y);
|
||||
}
|
||||
if (buttons & MouseButtonsMask.AuxiliaryButton) {
|
||||
emitMouseUp(MouseButton.AuxiliaryButton, x, y);
|
||||
}
|
||||
if (buttons & MouseButtonsMask.FourthButton) {
|
||||
emitMouseUp(MouseButton.FourthButton, x, y);
|
||||
}
|
||||
if (buttons & MouseButtonsMask.FifthButton) {
|
||||
emitMouseUp(MouseButton.FifthButton, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
// If the user has any mouse buttons pressed then press them again.
|
||||
function pressMouseButtons(buttons, x, y) {
|
||||
if (buttons & MouseButtonsMask.PrimaryButton) {
|
||||
emitMouseDown(MouseButton.MainButton, x, y);
|
||||
}
|
||||
if (buttons & MouseButtonsMask.SecondaryButton) {
|
||||
emitMouseDown(MouseButton.SecondaryButton, x, y);
|
||||
}
|
||||
if (buttons & MouseButtonsMask.AuxiliaryButton) {
|
||||
emitMouseDown(MouseButton.AuxiliaryButton, x, y);
|
||||
}
|
||||
if (buttons & MouseButtonsMask.FourthButton) {
|
||||
emitMouseDown(MouseButton.FourthButton, x, y);
|
||||
}
|
||||
if (buttons & MouseButtonsMask.FifthButton) {
|
||||
emitMouseDown(MouseButton.FifthButton, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
function registerInputs(playerElement) {
|
||||
if (!playerElement)
|
||||
return;
|
||||
|
||||
registerMouseEnterAndLeaveEvents(playerElement);
|
||||
}
|
||||
|
||||
function registerMouseEnterAndLeaveEvents(playerElement) {
|
||||
playerElement.onmouseenter = function(e) {
|
||||
let Data = new DataView(new ArrayBuffer(1));
|
||||
Data.setUint8(0, MessageType.MouseEnter);
|
||||
sendInputData(Data.buffer);
|
||||
playerElement.pressMouseButtons(e);
|
||||
};
|
||||
|
||||
playerElement.onmouseleave = function(e) {
|
||||
let Data = new DataView(new ArrayBuffer(1));
|
||||
Data.setUint8(0, MessageType.MouseLeave);
|
||||
sendInputData(Data.buffer);
|
||||
playerElement.releaseMouseButtons(e);
|
||||
};
|
||||
}
|
||||
|
||||
// A locked mouse works by the user clicking in the browser player and the
|
||||
// cursor disappears and is locked. The user moves the cursor and the camera
|
||||
// moves, for example. The user presses escape to free the mouse.
|
||||
function registerLockedMouseEvents(playerElement) {
|
||||
let x = playerElement.width / 2;
|
||||
let y = playerElement.height / 2;
|
||||
|
||||
playerElement.requestPointerLock = playerElement.requestPointerLock || playerElement.mozRequestPointerLock;
|
||||
document.exitPointerLock = document.exitPointerLock || document.mozExitPointerLock;
|
||||
|
||||
playerElement.onclick = function() {
|
||||
playerElement.requestPointerLock();
|
||||
};
|
||||
|
||||
// Respond to lock state change events
|
||||
document.addEventListener('pointerlockchange', lockStateChange, false);
|
||||
document.addEventListener('mozpointerlockchange', lockStateChange, false);
|
||||
|
||||
function lockStateChange() {
|
||||
if (document.pointerLockElement === playerElement ||
|
||||
document.mozPointerLockElement === playerElement) {
|
||||
console.log('Pointer locked');
|
||||
document.addEventListener("mousemove", updatePosition, false);
|
||||
} else {
|
||||
console.log('The pointer lock status is now unlocked');
|
||||
document.removeEventListener("mousemove", updatePosition, false);
|
||||
}
|
||||
}
|
||||
|
||||
function updatePosition(e) {
|
||||
x += e.movementX;
|
||||
y += e.movementY;
|
||||
if (x > styleWidth) {
|
||||
x -= styleWidth;
|
||||
}
|
||||
if (y > styleHeight) {
|
||||
y -= styleHeight;
|
||||
}
|
||||
if (x < 0) {
|
||||
x = styleWidth + x;
|
||||
}
|
||||
if (y < 0) {
|
||||
y = styleHeight - y;
|
||||
}
|
||||
emitMouseMove(x, y, e.movementX, e.movementY);
|
||||
}
|
||||
|
||||
playerElement.onmousedown = function(e) {
|
||||
emitMouseDown(e.button, x, y);
|
||||
};
|
||||
|
||||
playerElement.onmouseup = function(e) {
|
||||
emitMouseUp(e.button, x, y);
|
||||
};
|
||||
|
||||
playerElement.onmousewheel = function(e) {
|
||||
emitMouseWheel(e.wheelDelta, x, y);
|
||||
};
|
||||
|
||||
playerElement.pressMouseButtons = function(e) {
|
||||
pressMouseButtons(e.buttons, x, y);
|
||||
};
|
||||
|
||||
playerElement.releaseMouseButtons = function(e) {
|
||||
releaseMouseButtons(e.buttons, x, y);
|
||||
};
|
||||
}
|
||||
|
||||
// A hovering mouse works by the user clicking the mouse button when they want
|
||||
// the cursor to have an effect over the video. Otherwise the cursor just
|
||||
// passes over the browser.
|
||||
function registerHoveringMouseEvents(playerElement) {
|
||||
styleCursor = 'none'; // We will rely on UE4 client's software cursor.
|
||||
//styleCursor = 'default'; // Showing cursor
|
||||
|
||||
playerElement.onmousemove = function(e) {
|
||||
emitMouseMove(e.offsetX, e.offsetY, e.movementX, e.movementY);
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
playerElement.onmousedown = function(e) {
|
||||
emitMouseDown(e.button, e.offsetX, e.offsetY);
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
playerElement.onmouseup = function(e) {
|
||||
emitMouseUp(e.button, e.offsetX, e.offsetY);
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
// When the context menu is shown then it is safest to release the button
|
||||
// which was pressed when the event happened. This will guarantee we will
|
||||
// get at least one mouse up corresponding to a mouse down event. Otherwise
|
||||
// the mouse can get stuck.
|
||||
// https://github.com/facebook/react/issues/5531
|
||||
playerElement.oncontextmenu = function(e) {
|
||||
emitMouseUp(e.button, e.offsetX, e.offsetY);
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
if ('onmousewheel' in playerElement) {
|
||||
playerElement.onmousewheel = function(e) {
|
||||
emitMouseWheel(e.wheelDelta, e.offsetX, e.offsetY);
|
||||
e.preventDefault();
|
||||
};
|
||||
} else {
|
||||
playerElement.addEventListener('DOMMouseScroll', function(e) {
|
||||
emitMouseWheel(e.detail * -120, e.offsetX, e.offsetY);
|
||||
e.preventDefault();
|
||||
}, false);
|
||||
}
|
||||
|
||||
playerElement.pressMouseButtons = function(e) {
|
||||
pressMouseButtons(e.buttons, e.offsetX, e.offsetY);
|
||||
};
|
||||
|
||||
playerElement.releaseMouseButtons = function(e) {
|
||||
releaseMouseButtons(e.buttons, e.offsetX, e.offsetY);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Browser keys do not have a charCode so we only need to test keyCode.
|
||||
function isKeyCodeBrowserKey(keyCode) {
|
||||
// Function keys or tab key.
|
||||
return keyCode >= 112 && keyCode <= 123 || keyCode === 9;
|
||||
}
|
||||
|
||||
// Must be kept in sync with JavaScriptKeyCodeToFKey C++ array. The index of the
|
||||
// entry in the array is the special key code given below.
|
||||
const SpecialKeyCodes = {
|
||||
BackSpace: 8,
|
||||
Shift: 16,
|
||||
Control: 17,
|
||||
Alt: 18,
|
||||
RightShift: 253,
|
||||
RightControl: 254,
|
||||
RightAlt: 255
|
||||
};
|
||||
|
||||
// We want to be able to differentiate between left and right versions of some
|
||||
// keys.
|
||||
function getKeyCode(e) {
|
||||
if (e.keyCode === SpecialKeyCodes.Shift && e.code === 'ShiftRight') return SpecialKeyCodes.RightShift;
|
||||
else if (e.keyCode === SpecialKeyCodes.Control && e.code === 'ControlRight') return SpecialKeyCodes.RightControl;
|
||||
else if (e.keyCode === SpecialKeyCodes.Alt && e.code === 'AltRight') return SpecialKeyCodes.RightAlt;
|
||||
else return e.keyCode;
|
||||
}
|
||||
|
||||
function registerKeyboardEvents() {
|
||||
document.onkeydown = function(e) {
|
||||
sendInputData(new Uint8Array([MessageType.KeyDown, getKeyCode(e), e.repeat]).buffer);
|
||||
// Backspace is not considered a keypress in JavaScript but we need it
|
||||
// to be so characters may be deleted in a UE4 text entry field.
|
||||
if (e.keyCode === SpecialKeyCodes.BackSpace) {
|
||||
document.onkeypress({
|
||||
charCode: SpecialKeyCodes.BackSpace
|
||||
});
|
||||
}
|
||||
if (inputOptions.suppressBrowserKeys && isKeyCodeBrowserKey(e.keyCode)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
document.onkeyup = function(e) {
|
||||
sendInputData(new Uint8Array([MessageType.KeyUp, getKeyCode(e)]).buffer);
|
||||
if (inputOptions.suppressBrowserKeys && isKeyCodeBrowserKey(e.keyCode)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
document.onkeypress = function(e) {
|
||||
let data = new DataView(new ArrayBuffer(3));
|
||||
data.setUint8(0, MessageType.KeyPress);
|
||||
data.setUint16(1, e.charCode, true);
|
||||
sendInputData(data.buffer);
|
||||
};
|
||||
}
|
||||
|
||||
function start() {
|
||||
showConnectOverlay();
|
||||
if (webRtcPlayerObj) {
|
||||
webRtcPlayerObj.setVideoEnabled(true);
|
||||
}
|
||||
|
||||
|
||||
if (!connect_on_load || is_reconnection) {
|
||||
shouldShowPlayOverlay = true;
|
||||
resizePlayerStyle();
|
||||
} else {
|
||||
connect();
|
||||
}
|
||||
// connect();
|
||||
setTimeout(connect, 2000);
|
||||
}
|
||||
|
||||
function connect() {
|
||||
"use strict";
|
||||
|
||||
window.WebSocket = window.WebSocket || window.MozWebSocket;
|
||||
|
||||
if (!window.WebSocket) {
|
||||
alert('Your browser doesn\'t support WebSocket');
|
||||
return;
|
||||
}
|
||||
|
||||
ws = new WebSocket(window.location.href.replace('http://', 'ws://').replace('https://', 'wss://'));
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
console.log(`<- SS: ${event.data}`);
|
||||
let msg = JSON.parse(event.data);
|
||||
if (msg.type === 'config') {
|
||||
onConfig(msg);
|
||||
} else if (msg.type === 'answer') {
|
||||
onWebRtcAnswer(msg);
|
||||
} else if (msg.type === 'iceCandidate') {
|
||||
onWebRtcIce(msg.candidate);
|
||||
} else {
|
||||
console.log(`invalid SS message type: ${msg.type}`);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = function(event) {
|
||||
console.log(`WS error: ${JSON.stringify(event)}`);
|
||||
};
|
||||
|
||||
ws.onclose = function(event) {
|
||||
console.log(`WS closed: ${JSON.stringify(event.code)} - ${event.reason}`);
|
||||
ws = undefined;
|
||||
is_reconnection = true;
|
||||
|
||||
// destroy `webRtcPlayerObj` if any
|
||||
let playerDiv = document.getElementById('player');
|
||||
if (webRtcPlayerObj) {
|
||||
playerDiv.removeChild(webRtcPlayerObj.video);
|
||||
webRtcPlayerObj.close();
|
||||
webRtcPlayerObj = undefined;
|
||||
}
|
||||
|
||||
setTimeout(start, 4000);
|
||||
};
|
||||
}
|
||||
|
||||
// Config data received from WebRTC sender via the Cirrus web server
|
||||
function onConfig(config) {
|
||||
let playerDiv = document.getElementById('player');
|
||||
let playerElement = setupWebRtcPlayer(playerDiv, config);
|
||||
resizePlayerStyle();
|
||||
|
||||
switch (inputOptions.controlScheme) {
|
||||
case ControlSchemeType.HoveringMouse:
|
||||
registerHoveringMouseEvents(playerElement);
|
||||
break;
|
||||
case ControlSchemeType.LockedMouse:
|
||||
registerLockedMouseEvents(playerElement);
|
||||
break;
|
||||
default:
|
||||
console.log(`ERROR: Unknown control scheme ${inputOptions.controlScheme}`);
|
||||
registerLockedMouseEvents(playerElement);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function load() {
|
||||
registerKeyboardEvents();
|
||||
start();
|
||||
}
|
52
web/src/App.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<WebRTC />
|
||||
<div id="ui">
|
||||
<TopBar />
|
||||
<LeftPane />
|
||||
<RightPane />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WebRTC from './components/WebRTC.vue'
|
||||
import TopBar from './components/TopBar.vue'
|
||||
import LeftPane from "./components/LeftPane.vue"
|
||||
import RightPane from "./components/RightPane.vue"
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
WebRTC,
|
||||
TopBar,
|
||||
LeftPane,
|
||||
RightPane
|
||||
},
|
||||
mounted()
|
||||
{
|
||||
//this.webrtc.load();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html,body
|
||||
{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
#ui
|
||||
{
|
||||
position:absolute;
|
||||
width:100%;
|
||||
left:0;
|
||||
top:0;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
BIN
web/src/assets/images/car_perview0.png
Normal file
After ![]() (image error) Size: 86 KiB |
BIN
web/src/assets/images/left_pane_bd0_bg.png
Normal file
After ![]() (image error) Size: 10 KiB |
BIN
web/src/assets/images/left_pane_bd0_right_bg.png
Normal file
After ![]() (image error) Size: 3.1 KiB |
BIN
web/src/assets/images/left_pane_bd1_bg.png
Normal file
After ![]() (image error) Size: 12 KiB |
BIN
web/src/assets/images/left_pane_bd1_pipe_bg.png
Normal file
After ![]() (image error) Size: 2.4 KiB |
BIN
web/src/assets/images/left_pane_bd2_bg.png
Normal file
After ![]() (image error) Size: 10 KiB |
BIN
web/src/assets/images/peo_perview0.png
Normal file
After ![]() (image error) Size: 15 KiB |
BIN
web/src/assets/images/peo_perview1.png
Normal file
After ![]() (image error) Size: 14 KiB |
BIN
web/src/assets/images/right_pane_bd0_bg.png
Normal file
After ![]() (image error) Size: 11 KiB |
BIN
web/src/assets/images/right_pane_bd1_bg.png
Normal file
After ![]() (image error) Size: 12 KiB |
BIN
web/src/assets/images/right_pane_bd2_bg.png
Normal file
After ![]() (image error) Size: 11 KiB |
BIN
web/src/assets/images/top_bar_left_bg.png
Normal file
After ![]() (image error) Size: 30 KiB |
BIN
web/src/assets/images/top_bar_right_bg.png
Normal file
After ![]() (image error) Size: 25 KiB |
BIN
web/src/assets/logo.png
Normal file
After ![]() (image error) Size: 6.7 KiB |
81
web/src/components/BoardCar.vue
Normal file
@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div id="board_car">
|
||||
<span class="total">感应数量:{{ total }}辆/次</span>
|
||||
<div class="perviews">
|
||||
<div class="perview" v-for="(item,index) in induction" :key="index">
|
||||
<img :src="item.img"/>
|
||||
<span class="name">{{item.name}}</span>
|
||||
<span class="time">{{item.time}}</span>
|
||||
<span class="point">{{item.point}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: "BoardCar",
|
||||
data() {
|
||||
return {
|
||||
total: 2000,
|
||||
induction:[
|
||||
{img:require("../assets/images/car_perview0.png"),name:"京A023FD",time:"13:15",point:"东风小区南门"},
|
||||
{img:require("../assets/images/car_perview0.png"),name:"京A023FD",time:"15:15",point:"东风小区南门"},
|
||||
{img:require("../assets/images/car_perview0.png"),name:"京A023FD",time:"13:15",point:"东风小区南门"},
|
||||
{img:require("../assets/images/car_perview0.png"),name:"京A023FD",time:"11:17",point:"东风小区南门"},
|
||||
{img:require("../assets/images/car_perview0.png"),name:"京A023FD",time:"13:15",point:"东风小区南门"},
|
||||
{img:require("../assets/images/car_perview0.png"),name:"京A023FD",time:"13:19",point:"东风小区南门"},
|
||||
{img:require("../assets/images/car_perview0.png"),name:"京A023FD",time:"11:15",point:"东风小区南门"},
|
||||
{img:require("../assets/images/car_perview0.png"),name:"京A023FD",time:"13:15",point:"东风小区南门"},
|
||||
]
|
||||
};
|
||||
},
|
||||
methods: {},
|
||||
mounted() {},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
#board_car {
|
||||
width: 480px;
|
||||
height: 289px;
|
||||
background: url("../assets/images/right_pane_bd2_bg.png") no-repeat;
|
||||
background-size: contain;
|
||||
margin-top: 10px;
|
||||
}
|
||||
#board_car .total {
|
||||
float: right;
|
||||
color: #01d6d8;
|
||||
font-size: 14px;
|
||||
margin-top: 15px;
|
||||
margin-right: 35px;
|
||||
}
|
||||
#board_car .perviews {
|
||||
float: left;
|
||||
padding-left: 8px;
|
||||
padding-top: 14px;
|
||||
width: 100%;
|
||||
}
|
||||
#board_car .perview {
|
||||
float: left;
|
||||
width: 100px;
|
||||
height: 94px;
|
||||
margin-left: 10px;
|
||||
margin-top: 14px;
|
||||
padding:1px;
|
||||
border: #01d6d8 solid 1px;
|
||||
font-size: 10px;
|
||||
color:white;
|
||||
}
|
||||
|
||||
#board_car .name {
|
||||
float: left;
|
||||
margin-left: 2px;
|
||||
}
|
||||
#board_car .time {
|
||||
float: right;
|
||||
margin-right: 2px;
|
||||
}
|
||||
#board_car .point {
|
||||
float: left;
|
||||
margin-left: 2px;
|
||||
}
|
||||
</style>
|
133
web/src/components/BoardEvent.vue
Normal file
@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<div id="board_event">
|
||||
<div class="pipeview" v-for="(item,index) in events" :key="index">
|
||||
<span class="title">{{item.name}}</span>
|
||||
<div class="pipe" ref="pipe" />
|
||||
<span class="value0txt">{{item.value[0]}}</span>
|
||||
<span class="value1txt">{{item.value[1]}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: "BoardEvent",
|
||||
data()
|
||||
{
|
||||
return {
|
||||
events:[
|
||||
{name:"东风小区",value:[10120,10120*0.5]},
|
||||
{name:"西风小区",value:[20120,20120*0.85]},
|
||||
{name:"北风小区",value:[30120,30120*0.75]},
|
||||
{name:"南风小区",value:[3120,3120*0.5]},
|
||||
],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
createPipe(el, data) {
|
||||
let pipe = this.echarts.init(el);
|
||||
let option = {
|
||||
animation:true,
|
||||
animationDuration:5000,
|
||||
animationEasing:'bounceOut',
|
||||
title: {
|
||||
itemGap:0,
|
||||
text: Math.floor(data.value[1]/(data.value[0])*100)+"%",
|
||||
subtext: "处置率",
|
||||
left: "center",
|
||||
top: "center",
|
||||
textStyle: {
|
||||
fontSize: 15,
|
||||
color: "#ffffff",
|
||||
},
|
||||
subtextStyle: {
|
||||
fontSize: 10,
|
||||
color: "#01e0e2",
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
},
|
||||
color: ["#0357b6", "#00e4f1"],
|
||||
series: [
|
||||
{
|
||||
name: "Access From",
|
||||
type: "pie",
|
||||
radius: ["70%", "90%"],
|
||||
startAngle:-180,
|
||||
avoidLabelOverlap: true,
|
||||
label: {
|
||||
show: false,
|
||||
},
|
||||
data: [
|
||||
{ value: data.value[0], name: "处置" },
|
||||
{ value: data.value[1], name: "发生" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
pipe.setOption(option);
|
||||
window.addEventListener("resize",()=>
|
||||
{
|
||||
pipe.resize();
|
||||
pipe.clear(); pipe.setOption(option);
|
||||
});
|
||||
setTimeout(()=>{pipe.resize();pipe.clear(); pipe.setOption(option);},200);
|
||||
},
|
||||
},
|
||||
mounted()
|
||||
{
|
||||
for(var i=0;i<this.events.length;i++)
|
||||
{
|
||||
this.createPipe(this.$refs.pipe[i],this.events[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
#board_event {
|
||||
width: 484px;
|
||||
height: 335px;
|
||||
background: url("../assets/images/left_pane_bd1_bg.png") no-repeat;
|
||||
background-size: contain;
|
||||
margin-top: 10px;
|
||||
padding-top: 48px;
|
||||
padding-left: 6px;
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
}
|
||||
#board_event .pipeview {
|
||||
float: left;
|
||||
width: 230px;
|
||||
height: 131px;
|
||||
background: url("../assets/images/left_pane_bd1_pipe_bg.png") no-repeat;
|
||||
background-size: contain;
|
||||
margin-top: 10px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
#board_event .pipe {
|
||||
clear: left;
|
||||
float: left;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-top: 15px;
|
||||
margin-left: 35px;
|
||||
}
|
||||
#board_event .title {
|
||||
float: left;
|
||||
font-size: 14px;
|
||||
margin-top: 10px;
|
||||
margin-left: 33px;
|
||||
}
|
||||
#board_event .value0txt
|
||||
{
|
||||
float:left;
|
||||
margin-left:40px;
|
||||
margin-top:20px;
|
||||
}
|
||||
#board_event .value1txt
|
||||
{
|
||||
float:left;
|
||||
margin-left:40px;
|
||||
margin-top:35px;
|
||||
}
|
||||
</style>
|
76
web/src/components/BoardEventRT.vue
Normal file
@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div id="board_event_rt">
|
||||
<table>
|
||||
<tr class="dark">
|
||||
<th width="15%">时间</th>
|
||||
<th width="35%">地点</th>
|
||||
<th width="35%">事件</th>
|
||||
<th width="15%">状态</th>
|
||||
</tr>
|
||||
<tr v-for="(item,index) in events" :key="index" :class="index%2==0?'light':'dark'">
|
||||
<td width="15%" >{{item.time}}</td>
|
||||
<td width="35%">{{item.place}}</td>
|
||||
<td width="35%" >{{item.event}}</td>
|
||||
<td width="15%">{{item.state}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name:'BoardEventRT',
|
||||
data()
|
||||
{
|
||||
return {
|
||||
events:
|
||||
[
|
||||
{time:"13:00",place:"南风小区",event:"寻衅滋事",state:"已处置"},
|
||||
{time:"13:00",place:"南风小区",event:"寻衅滋事",state:"已处置"},
|
||||
{time:"13:00",place:"南风小区",event:"寻衅滋事",state:"已处置"},
|
||||
{time:"13:00",place:"南风小区",event:"寻衅滋事",state:"已处置"},
|
||||
{time:"13:00",place:"南风小区",event:"寻衅滋事",state:"已处置"},
|
||||
{time:"13:00",place:"南风小区",event:"寻衅滋事",state:"已处置"},
|
||||
{time:"13:00",place:"南风小区",event:"寻衅滋事",state:"已处置"},
|
||||
{time:"13:00",place:"南风小区",event:"寻衅滋事",state:"已处置"},
|
||||
{time:"13:00",place:"南风小区",event:"寻衅滋事",state:"已处置"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
#board_event_rt
|
||||
{
|
||||
width: 484px;
|
||||
height: 354px;
|
||||
background: url("../assets/images/left_pane_bd2_bg.png") no-repeat;
|
||||
background-size: contain;
|
||||
margin-top: -35px;
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
}
|
||||
#board_event_rt table
|
||||
{
|
||||
float:left;
|
||||
width:456px;
|
||||
height:288px;
|
||||
margin-top: 55px;
|
||||
margin-left: 16px;
|
||||
border:0;
|
||||
padding:0;
|
||||
}
|
||||
#board_event_rt table tr
|
||||
{
|
||||
height:26px;
|
||||
vertical-align: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.dark
|
||||
{
|
||||
background-color:rgba(13, 94, 210, 0.3);
|
||||
}
|
||||
.light
|
||||
{
|
||||
background-color: rgba(13, 94, 210, 0.1);
|
||||
}
|
||||
</style>
|
198
web/src/components/BoardGate.vue
Normal file
@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<div id="board_gate">
|
||||
<div class="line" ref="line" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: "BoardGate",
|
||||
methods: {
|
||||
createLine(el, data) {
|
||||
let line = this.echarts.init(el);
|
||||
let option = {
|
||||
color: ["#31b9ff", "#ff6262", "#ffa200"],
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: {
|
||||
type: "cross",
|
||||
label: {
|
||||
backgroundColor: "#6a7985",
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
itemGap: 20,
|
||||
itemWidth: 14,
|
||||
itemHeight: 14,
|
||||
data: [
|
||||
{ icon: "rect", name: "通行人次" },
|
||||
{ icon: "rect", name: "通行车辆" },
|
||||
{ icon: "rect", name: "通行非机动车次" },
|
||||
],
|
||||
textStyle: {
|
||||
color: "#ffffff",
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: "3%",
|
||||
right: "4%",
|
||||
bottom: "3%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: "category",
|
||||
boundaryGap: false,
|
||||
data: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"],
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
verticalAlign: "middle",
|
||||
margin: 12,
|
||||
textStyle: {
|
||||
color: "#FFFFFF",
|
||||
fontSize: 8,
|
||||
lineHeight: 20,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: "value",
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: "rgba(255,255,255,.2)",
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
verticalAlign: "top",
|
||||
textStyle: {
|
||||
color: "#FFFFFF",
|
||||
fontSize: 8,
|
||||
lineHeight: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: "通行人次",
|
||||
type: "line",
|
||||
stack: "Total",
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
},
|
||||
showSymbol: false,
|
||||
areaStyle: {
|
||||
opacity: 0.4,
|
||||
color: new this.echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: "rgb(49, 185, 255)",
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: "rgb(40, 150, 207)",
|
||||
},
|
||||
]),
|
||||
},
|
||||
emphasis: {
|
||||
focus: "series",
|
||||
},
|
||||
data: data[0],
|
||||
},
|
||||
{
|
||||
name: "通行车辆",
|
||||
type: "line",
|
||||
stack: "Total",
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
},
|
||||
showSymbol: false,
|
||||
areaStyle: {
|
||||
opacity: 0.4,
|
||||
color: new this.echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: "rgb(255, 98, 98)",
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: "rgb(151, 58, 58)",
|
||||
},
|
||||
]),
|
||||
},
|
||||
emphasis: {
|
||||
focus: "series",
|
||||
},
|
||||
data: data[1],
|
||||
},
|
||||
{
|
||||
name: "通行非机动车次",
|
||||
type: "line",
|
||||
stack: "Total",
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
},
|
||||
showSymbol: false,
|
||||
areaStyle: {
|
||||
opacity: 0.4,
|
||||
color: new this.echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: "rgb(255, 162, 0)",
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: "rgb(200, 136, 0)",
|
||||
},
|
||||
]),
|
||||
},
|
||||
emphasis: {
|
||||
focus: "series",
|
||||
},
|
||||
data: data[2],
|
||||
},
|
||||
],
|
||||
};
|
||||
line.setOption(option);
|
||||
window.addEventListener("resize", () => {
|
||||
line.resize();
|
||||
line.clear();
|
||||
line.setOption(option);
|
||||
});
|
||||
setTimeout(() => {
|
||||
line.resize();
|
||||
line.clear();
|
||||
line.setOption(option);
|
||||
}, 200);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
console.log(this.echarts);
|
||||
this.createLine(this.$refs.line, [
|
||||
[140, 232, 101, 264, 90, 340, 250],
|
||||
[120, 282, 111, 234, 220, 340, 310],
|
||||
[320, 132, 201, 334, 190, 130, 220],
|
||||
]);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
#board_gate {
|
||||
width: 480px;
|
||||
height: 283px;
|
||||
background: url("../assets/images/right_pane_bd0_bg.png") no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
#board_gate .line {
|
||||
width: 420px;
|
||||
height: 220px;
|
||||
padding-top: 60px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
</style>
|
85
web/src/components/BoardPeo.vue
Normal file
@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div id="board_peo">
|
||||
<span class="total">感应数量:{{ total }}人/次</span>
|
||||
<div class="perviews">
|
||||
<div class="perview" v-for="(item,index) in induction" :key="index">
|
||||
<img :src="item.img"/>
|
||||
<span class="name">{{item.name}}</span>
|
||||
<span class="time">{{item.time}}</span>
|
||||
<span class="point">{{item.point}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: "BoardPeo",
|
||||
data() {
|
||||
return {
|
||||
total: 2000,
|
||||
induction:[
|
||||
{img:require("../assets/images/peo_perview0.png"),name:"王小二",time:"13:15",point:"东风小区南门"},
|
||||
{img:require("../assets/images/peo_perview1.png"),name:"王小二",time:"15:15",point:"东风小区南门"},
|
||||
{img:require("../assets/images/peo_perview0.png"),name:"王小二",time:"13:15",point:"东风小区南门"},
|
||||
{img:require("../assets/images/peo_perview1.png"),name:"王小二",time:"11:17",point:"东风小区南门"},
|
||||
{img:require("../assets/images/peo_perview0.png"),name:"王小二",time:"13:15",point:"东风小区南门"},
|
||||
{img:require("../assets/images/peo_perview1.png"),name:"王小二",time:"13:19",point:"东风小区南门"},
|
||||
{img:require("../assets/images/peo_perview0.png"),name:"王小二",time:"11:15",point:"东风小区南门"},
|
||||
{img:require("../assets/images/peo_perview1.png"),name:"王小二",time:"13:15",point:"东风小区南门"},
|
||||
{img:require("../assets/images/peo_perview0.png"),name:"王小二",time:"12:35",point:"东风小区南门"},
|
||||
{img:require("../assets/images/peo_perview1.png"),name:"王小二",time:"13:15",point:"东风小区南门"},
|
||||
{img:require("../assets/images/peo_perview0.png"),name:"王小二",time:"14:25",point:"东风小区南门"},
|
||||
{img:require("../assets/images/peo_perview1.png"),name:"王小二",time:"16:18",point:"东风小区南门"},
|
||||
]
|
||||
};
|
||||
},
|
||||
methods: {},
|
||||
mounted() {},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
#board_peo {
|
||||
width: 480px;
|
||||
height: 370px;
|
||||
background: url("../assets/images/right_pane_bd1_bg.png") no-repeat;
|
||||
background-size: contain;
|
||||
margin-top: 10px;
|
||||
}
|
||||
#board_peo .total {
|
||||
float: right;
|
||||
color: #01d6d8;
|
||||
font-size: 14px;
|
||||
margin-top: 15px;
|
||||
margin-right: 35px;
|
||||
}
|
||||
#board_peo .perviews {
|
||||
float: left;
|
||||
padding-left: 3px;
|
||||
padding-top: 14px;
|
||||
width: 100%;
|
||||
}
|
||||
#board_peo .perview {
|
||||
float: left;
|
||||
width: 100px;
|
||||
height: 94px;
|
||||
margin-left: 12px;
|
||||
margin-top: 6px;
|
||||
padding:1px;
|
||||
border: #01d6d8 solid 1px;
|
||||
font-size: 10px;
|
||||
color:white;
|
||||
}
|
||||
|
||||
#board_peo .name {
|
||||
float: left;
|
||||
margin-left: 2px;
|
||||
}
|
||||
#board_peo .time {
|
||||
float: right;
|
||||
margin-right: 2px;
|
||||
}
|
||||
#board_peo .point {
|
||||
float: left;
|
||||
margin-left: 2px;
|
||||
}
|
||||
</style>
|
112
web/src/components/BoardPop.vue
Normal file
@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div id="board_pop">
|
||||
<div class="pipe" ref="pipe" />
|
||||
<div class="nums">
|
||||
<div class="numtxt">
|
||||
<!-- TODO 数字动画 -->
|
||||
实有人数<span>32901</span>人
|
||||
</div>
|
||||
<div class="numtxt">
|
||||
常驻人口<span>27901</span>人
|
||||
</div>
|
||||
<div class="numtxt">
|
||||
流动人口<span>5000</span>人
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: "BoardPop",
|
||||
methods: {
|
||||
//TODO 抽取饼图部件复用
|
||||
initPipe() {
|
||||
let pipe = this.echarts.init(this.$refs.pipe);
|
||||
let option = {
|
||||
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
},
|
||||
color:['#6b0edb','#ff7723','#0279ff'],
|
||||
series: [
|
||||
{
|
||||
name: "Access From",
|
||||
type: "pie",
|
||||
radius: "90%",
|
||||
selectedMode:'single',
|
||||
label:{
|
||||
position:'inner',
|
||||
fontSize:14,
|
||||
color:"#ffffff"
|
||||
},
|
||||
labelLine:
|
||||
{
|
||||
show:false
|
||||
},
|
||||
data: [
|
||||
{ value: 1048, name: "实有人数"},
|
||||
{ value: 735, name: "流动人口" },
|
||||
{ value: 580, name: "常驻人口" }
|
||||
],
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: "rgba(0, 0, 0, 0.5)",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
pipe.setOption(option);
|
||||
window.addEventListener("resize",()=>
|
||||
{
|
||||
pipe.resize();
|
||||
pipe.clear(); pipe.setOption(option);
|
||||
});
|
||||
setTimeout(()=>{pipe.resize();pipe.clear(); pipe.setOption(option);},200);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.initPipe();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
#board_pop {
|
||||
width: 484px;
|
||||
height: 250px;
|
||||
background: url("../assets/images/left_pane_bd0_bg.png") no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
#board_pop .pipe {
|
||||
float:left;
|
||||
width: 186px;
|
||||
height: 186px;
|
||||
margin-top: 52px;
|
||||
margin-left: 35px;
|
||||
|
||||
}
|
||||
#board_pop .nums {
|
||||
float:left;
|
||||
width: 240px;
|
||||
height: 186px;
|
||||
margin-top: 52px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
#board_pop .numtxt {
|
||||
width: 235px;
|
||||
height:61px;
|
||||
background: url("../assets/images/left_pane_bd0_right_bg.png") no-repeat;
|
||||
background-size: cover;
|
||||
font-size: 18px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
line-height: 61px;
|
||||
}
|
||||
#board_pop span
|
||||
{
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
58
web/src/components/HelloWorld.vue
Normal file
@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="hello">
|
||||
<h1>{{ msg }}</h1>
|
||||
<p>
|
||||
For a guide and recipes on how to configure / customize this project,<br>
|
||||
check out the
|
||||
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
|
||||
</p>
|
||||
<h3>Installed CLI Plugins</h3>
|
||||
<ul>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
|
||||
</ul>
|
||||
<h3>Essential Links</h3>
|
||||
<ul>
|
||||
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
|
||||
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
|
||||
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
|
||||
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
|
||||
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
|
||||
</ul>
|
||||
<h3>Ecosystem</h3>
|
||||
<ul>
|
||||
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
|
||||
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
|
||||
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
|
||||
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
|
||||
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'HelloWorld',
|
||||
props: {
|
||||
msg: String
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
h3 {
|
||||
margin: 40px 0 0;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
}
|
||||
a {
|
||||
color: #42b983;
|
||||
}
|
||||
</style>
|
31
web/src/components/LeftPane.vue
Normal file
@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div id="left_pane">
|
||||
<BoardPop/>
|
||||
<BoardEvent/>
|
||||
<BoardEventRT/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import BoardPop from './BoardPop.vue'
|
||||
import BoardEvent from './BoardEvent.vue'
|
||||
import BoardEventRT from './BoardEventRT.vue'
|
||||
|
||||
export default {
|
||||
name:'LeftPane',
|
||||
components: {
|
||||
BoardPop,
|
||||
BoardEvent,
|
||||
BoardEventRT
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
#left_pane
|
||||
{
|
||||
clear:both;
|
||||
float:left;
|
||||
height:100%;
|
||||
padding-left: 19px;
|
||||
pointer-events:auto;
|
||||
}
|
||||
</style>
|
59
web/src/components/PixelStream.vue
Normal file
@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div id="pixel-stream">
|
||||
<video ref="videoElement" autoplay playsinline></video>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SimplePeer from 'simple-peer';
|
||||
|
||||
export default {
|
||||
name: 'PixelStream',
|
||||
data() {
|
||||
return {
|
||||
peer: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.setupPixelStream();
|
||||
},
|
||||
methods: {
|
||||
setupPixelStream() {
|
||||
// Example setup for WebRTC connection
|
||||
this.peer = new SimplePeer({
|
||||
initiator: true,
|
||||
trickle: false,
|
||||
});
|
||||
|
||||
this.peer.on('signal', data => {
|
||||
// Send signal data to the server
|
||||
console.log('Signal data:', data);
|
||||
});
|
||||
|
||||
this.peer.on('stream', stream => {
|
||||
// Attach the stream to the video element
|
||||
this.$refs.videoElement.srcObject = stream;
|
||||
});
|
||||
|
||||
// Example: Connect to the UE server
|
||||
// You need to implement the signaling server logic
|
||||
// this.connectToSignalingServer();
|
||||
},
|
||||
connectToSignalingServer() {
|
||||
// Implement your signaling server connection logic here
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#pixel-stream {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: black;
|
||||
}
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
30
web/src/components/RightPane.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div id="right_pane">
|
||||
<BoardGate/>
|
||||
<BoardPeo/>
|
||||
<BoardCar/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import BoardGate from './BoardGate.vue'
|
||||
import BoardPeo from './BoardPeo.vue'
|
||||
import BoardCar from './BoardCar.vue'
|
||||
export default {
|
||||
name:'RightPane',
|
||||
components:
|
||||
{
|
||||
BoardGate,
|
||||
BoardPeo,
|
||||
BoardCar
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
#right_pane
|
||||
{
|
||||
float:right;
|
||||
height:100%;
|
||||
padding-right: 19px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
</style>
|
79
web/src/components/TopBar.vue
Normal file
@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div id="top_bar">
|
||||
<div id="top_bar_left">
|
||||
<span>XXXXXXX管理平台</span>
|
||||
</div>
|
||||
<div id="top_bar_right">
|
||||
<span class="time">19:00</span>
|
||||
<span class="date">2022年5月8日<br>星期天</span>
|
||||
<span class="temp">10-15℃</span>
|
||||
<span class="weather">晴转多云<br>微风≤2级</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name:'TopBar',
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
#top_bar
|
||||
{
|
||||
width:100%;
|
||||
color: white;
|
||||
pointer-events: auto;
|
||||
}
|
||||
#top_bar_left
|
||||
{
|
||||
float:left;
|
||||
width:723px;
|
||||
height:100px;
|
||||
background: url('../assets/images/top_bar_left_bg.png') no-repeat;
|
||||
background-size: cover;
|
||||
font-size:50px;
|
||||
text-align:left;
|
||||
font-weight: bold;
|
||||
padding-left: 50px;
|
||||
line-height:80px;
|
||||
}
|
||||
#top_bar_right
|
||||
{
|
||||
float:right;
|
||||
width:914px;
|
||||
height:70px;
|
||||
background: url('../assets/images/top_bar_right_bg.png') no-repeat;
|
||||
background-size: cover;
|
||||
line-height: 70px;
|
||||
|
||||
}
|
||||
#top_bar_right .time
|
||||
{
|
||||
float:left;
|
||||
margin-left: 100px;
|
||||
font-size: 35px;
|
||||
font-weight: bold;
|
||||
}
|
||||
#top_bar_right .date
|
||||
{
|
||||
float:left;
|
||||
margin-left: 10px;
|
||||
font-size: 25px;
|
||||
line-height: 30px;
|
||||
padding-top:5px;
|
||||
}
|
||||
#top_bar_right .temp
|
||||
{
|
||||
float:left;
|
||||
margin-left: 160px;
|
||||
font-size: 35px;
|
||||
font-weight: bold;
|
||||
}
|
||||
#top_bar_right .weather
|
||||
{
|
||||
float:left;
|
||||
margin-left: 10px;
|
||||
font-size: 25px;
|
||||
line-height: 30px;
|
||||
padding-top:5px;
|
||||
}
|
||||
</style>
|
25
web/src/components/WebRTC.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div id="web_rtc_view">
|
||||
<div id="web_rtc_player">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import webrtc from '../webrtc';
|
||||
export default
|
||||
{
|
||||
name:'WebRTC',
|
||||
mounted()
|
||||
{
|
||||
webrtc.webrtc.load();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
#web_rtc_view
|
||||
{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #575f69;
|
||||
}
|
||||
</style>
|
12
web/src/main.js
Normal file
@ -0,0 +1,12 @@
|
||||
import './utils/rem'
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import webrtc from './webrtc'
|
||||
import * as echarts from 'echarts'
|
||||
Vue.prototype.webrtc = webrtc.webrtc
|
||||
Vue.prototype.echarts = echarts
|
||||
Vue.config.productionTip = false
|
||||
|
||||
new Vue({
|
||||
render: h => h(App),
|
||||
}).$mount('#app')
|
18
web/src/utils/rem.js
Normal file
@ -0,0 +1,18 @@
|
||||
(function(doc, win) {
|
||||
var docEl = doc.documentElement;
|
||||
var resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize'
|
||||
var recalc = function() {
|
||||
var clientWidth = docEl.clientWidth
|
||||
if (!clientWidth) return
|
||||
|
||||
var html = document.getElementsByTagName('html')[0]
|
||||
var oWidth = document.body.clientWidth || document.documentElement.clientWidth
|
||||
oWidth = oWidth>1920?1920:oWidth;
|
||||
html.style.fontSize = oWidth / (1920/100) + 'px'
|
||||
}
|
||||
|
||||
if (!doc.addEventListener) return
|
||||
win.addEventListener(resizeEvt, recalc)
|
||||
|
||||
doc.addEventListener('DOMContentLoaded', recalc, false)
|
||||
})(document, window)
|
1944
web/src/webrtc.js
Normal file
18
web/vue.config.js
Normal file
@ -0,0 +1,18 @@
|
||||
module.exports=
|
||||
{
|
||||
css:
|
||||
{
|
||||
loaderOptions:
|
||||
{
|
||||
css:{},
|
||||
postcss:
|
||||
{
|
||||
plugins:[
|
||||
require('postcss-px2rem')({
|
||||
remUnit:100
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|