Commit ca6121f6 authored by rockyl's avatar rockyl

init

parents
Pipeline #293046 failed with stages
in 0 seconds
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# video-sender
## Project setup
```
yarn install
```
### Compiles and hot-reloads for development
```
yarn run serve
```
### Compiles and minifies for production
```
yarn run build
```
### Run your tests
```
yarn run test
```
### Lints and fixes files
```
yarn run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
chrome.app.runtime.onLaunched.addListener(function () {
/*chrome.app.window.create('dist/index.html', {
bounds: {
top: 0,
left: 0,
width: 400,
height: 640,
}
});*/
});
class HttpHandler extends WSC.BaseHandler {
get() {
this.write('hello');
this.finish();
}
}
let handlers = [
['.*', HttpHandler],
];
const options = {
port: '8088',
handlers: handlers,
optAllInterfaces: true,
};
const app = new WSC.WebApplication(options);
app.start(function () {
console.log('web http server started');
});
File added
{
"name": "web-http-server",
"version": "1.0",
"manifest_version": 2,
"minimum_chrome_version": "23",
"description": "web-http-server",
"app": {
"background": {
"scripts": [
"wsc-chrome.min.js",
"background.js"
]
}
},
"icons": {
"16": "assets/icon-16.png",
"48": "assets/icon-48.png",
"128": "assets/icon-128.png",
"256": "assets/icon-256.png"
},
"permissions": [
"alarms",
"unlimitedStorage",
"storage",
"notifications",
"browser",
"power",
"system.network",
{
"fileSystem": [
"write",
"directory",
"retainEntries"
]
}
],
"sockets": {
"udp": {
"send": [
""
],
"bind": [
""
],
"multicastMembership": ""
},
"tcp": {
"connect": [
""
]
},
"tcpServer": {
"listen": [
"*:*"
]
}
}
}
{
"name": "web-http-server",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
},
"dependencies": {
"core-js": "^3.3.2",
"vue": "^2.6.10",
"vuex": "^3.0.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.0.0",
"@vue/cli-service": "^4.0.0",
"vue-template-compiler": "^2.6.10"
}
}
module.exports = {
plugins: {
autoprefixer: {}
}
}
<!DOCTYPE html>
<html lang="en">
<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">
<title>video-sender</title>
</head>
<body>
<noscript>
<strong>We're sorry but video-sender 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>
<template>
<div id="app">
<h3 class="title">Video Sender</h3>
<div class="fields-wrapper">
<div class="field-item">
Ports:
<label>
<select :disabled="playing" v-model="port">
<option v-for="port in ports" :value="port.path">{{port.path}}</option>
</select>
</label>
<div class="inline">
<label>
Bitrate:
<input :disabled="playing" class="input" type="text" v-model="bitrate"></input>
</label>
<button :disabled="playing" @click="onClickReload">Reload</button>
<button :disabled="playing || !port || status > 0" @click="onClickConnect">Connect</button>
<button :disabled="playing || status !== 2" @click="onClickDisconnect">Disconnect</button>
</div>
</div>
<div class="field-item">
<div class="inline">
Status:
<div class="status" :style="{'background-color': status === 2 ? 'limegreen' : 'orangered'}"></div>
<span>{{statusStr}}</span>
</div>
</div>
<div class="field-item">
<label class="inline">
Fps:
<input :disabled="playing" type="range" min="1" max="60" v-model="fps" step="1">
<span>{{fps}}</span>
</label>
</div>
<div class="field-item">
<label class="inline">
Gray threshold:
<input type="range" min="0" max="255" v-model="grayThreshold" step="1">
<span>{{grayThreshold}}</span>
</label>
</div>
<div class="field-item">
<label class="inline">
Video:
<input type="file" @change="onSelectVideoFile" :disabled="playing"></input>
</label>
<label class="inline">
Volume:
<input type="range" min="0" max="1" v-model="volume" step="0.01">
</label>
</div>
<div class="field-item">
Frame:
<span>{{frameCount}}</span>
</div>
<div class="field-item">
Performance:
<p class="inline">
<span>local:{{(localFrameDeal/frameCount).toFixed(2)}}</span>
<span>mcu:{{(remoteFrameDeal/frameCount).toFixed(2)}}</span>
</p>
</div>
<div class="field-item">
Controls:
<button :disabled="!playable || playing" @click="onClickPlay">Play</button>
<button :disabled="!playable || !playing" @click="onClickPause">Pause</button>
<button :disabled="!playable" @click="onClickReset">Reset</button>
</div>
<div class="field-item">
Preview:
<p class="inline">
<label>
Width:
<input :disabled="playing" class="input" type="text" v-model="canvasSize.width" placeholder="width"></input>
</label>
<label>
Height:
<input :disabled="playing" class="input" type="text" v-model="canvasSize.height"
placeholder="height"></input>
</label>
</p>
<canvas ref="canvas" class="preview-canvas" :width="canvasSize.width" :height="canvasSize.height"></canvas>
<video ref="video" :src="videoUrl" v-show="false"></video>
</div>
</div>
</div>
</template>
<script>
const statusMapping = {
0: 'no connect',
1: 'connecting',
2: 'connected',
};
export default {
name: 'app',
components: {},
data() {
return {
ports: [],
port: null,
bitrate: 115200,
status: 0,
fps: 24,
grayThreshold: 128,
volume: 0.1,
frameCount: 0,
frameBeginLocal: 0,
frameBeginMCU: 0,
localFrameDeal: 0,
remoteFrameDeal: 0,
canvasSize: {
width: 128,
height: 64,
},
playing: false,
videoUrl: null,
waitForNextFrame: false,
playTimer: null,
canvasCtx: null,
connectionId: -1,
}
},
watch: {
volume() {
this.onVolumeChanged();
},
},
computed: {
statusStr() {
return statusMapping[this.status];
},
playable() {
return this.videoUrl && this.status === 2;
}
},
mounted() {
this.canvasCtx = this.$refs.canvas.getContext('2d');
this.onClickReload();
},
methods: {
async onClickReload() {
this.ports = await this.getPorts();
},
async onClickConnect() {
this.status = 1;
try {
this.connectionId = await this.connectToPort(this.port, this.bitrate);
this.status = 2;
chrome.serial.onReceive.addListener((e)=>{
//console.log(e);
this.onGotMessage(e);
});
} catch (e) {
console.log(`Can't connect to port: ${this.port}`);
this.status = 0;
}
},
async onClickDisconnect() {
try {
await this.disconnectPort();
this.status = 0;
} catch (e) {
console.log(`Can't disconnect port: ${this.port}`);
}
},
onGotMessage(readInfo){
let status = String.fromCharCode.apply(null, new Uint8Array(readInfo.data));
if (status === 'OK') {
this.remoteFrameDeal += Date.now() - this.frameBeginMCU;
this.waitForNextFrame = true;
}
},
onSelectVideoFile(e) {
let file = e.target.files[0];
if (file) {
this.videoUrl = URL.createObjectURL(file);
this.onClickReset();
}
},
onVolumeChanged() {
this.$refs.video.volume = this.volume;
},
onClickPlay() {
this.onVolumeChanged();
this.$refs.video.play();
this.playing = true;
this.startSendFrame();
},
onClickPause() {
this.$refs.video.pause();
this.playing = false;
this.stopSendFrame();
},
onClickReset() {
this.onClickPause();
this.$refs.video.currentTime = 0;
},
startSendFrame() {
this.frameCount = 0;
this.localFrameDeal = 0;
this.remoteFrameDeal = 0;
this.waitForNextFrame = true;
this.playTimer = setInterval(() => {
this.sendFrame();
}, 1000 / this.fps);
},
stopSendFrame(){
clearInterval(this.playTimer);
},
sendFrame() {
const {canvas, video} = this.$refs;
const {width, height} = canvas;
if (!this.videoSize) {
const {videoWidth, videoHeight} = video;
const ratioW = width / videoWidth;
const ratioH = height / videoHeight;
const ratio = Math.min(ratioW, ratioH);
this.videoSize = {width: videoWidth * ratio, height: videoHeight * ratio};
}
if (video.ended || video.paused) {
this.onClickReset();
return;
}
this.canvasCtx.drawImage(video, (width - this.videoSize.width) / 2, (height - this.videoSize.height) / 2, this.videoSize.width, this.videoSize.height);
if (this.waitForNextFrame) {
this.frameBeginLocal = Date.now();
this.waitForNextFrame = false;
const imgData = this.canvasCtx.getImageData(0, 0, width, height).data;
const output = [];
for (let page = 0; page < 8; page++) {
for (let col = 0; col < width; col++) {
let byte = 0;
for (let bit = 0; bit < 8; bit++) {
let index = (page * width * 8 + col + bit * width) * 4;
let r = imgData[index];
let g = imgData[index + 1];
let b = imgData[index + 2];
let gray = (r * 28 + g * 151 + b * 77) >> 8;
if (gray > this.grayThreshold) {
byte += 1 << bit;
}
}
output.push(byte);
}
}
this.send(output);
//console.time('frame');
this.frameBeginMCU = Date.now();
this.localFrameDeal += this.frameBeginMCU - this.frameBeginLocal;
}
this.frameCount++;
},
async getPorts() {
return new Promise(resolve => {
/*resolve([
{path: '/dev/aaa'},
{path: '/dev/bbb'},
{path: '/dev/ccc'},
]);*/
chrome.serial.getDevices(resolve);
});
},
async connectToPort(port, bitrate) {
return new Promise((resolve, reject) => {
/*setTimeout(() => {
resolve();
}, 1000);*/
chrome.serial.connect(port, {bitrate}, (openInfo)=>{
console.log(openInfo);
if (openInfo.connectionId < 0) {
reject();
}else{
resolve(openInfo.connectionId);
}
});
})
},
async disconnectPort() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 1000);
})
},
async send(data) {
return new Promise((resolve, reject) => {
let len = data.length;
let frame = [];
const frameHeader = 'FRM';
for (let i = 0; i < frameHeader.length; i++) {
frame.push(frameHeader.charCodeAt(i));
}
let b = new ArrayBuffer(2);
let vb = new DataView(b);
vb.setUint16(0, len, false);
let array = Array.prototype.slice.call(new Uint8Array(b ));
frame.push(array[0], array[1]);
frame = frame.concat(data);
let buffer = new Uint8Array(frame).buffer;
chrome.serial.send(this.connectionId, buffer, (info)=>{
//console.log('sended', info);
resolve();
});
//chrome.serial.flush(connectionId, onFlush);
})
}
}
}
</script>
<style>
html, body, #app {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
border: 0;
}
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
display: flex;
flex-direction: column;
}
button {
margin-left: 5px;
}
.title {
padding: 0 20px;
}
.fields-wrapper {
flex: 1;
overflow: hidden auto;
border: 1px solid lightgray;
}
.fields-wrapper::-webkit-scrollbar {
width: 4px;
height: 4px;
}
.fields-wrapper::-webkit-scrollbar-thumb {
border-radius: 5px;
background: rgba(0, 0, 0, 0.2);
}
.fields-wrapper::-webkit-scrollbar-track {
}
.field-item {
padding: 10px;
}
.field-item + .field-item {
border-top: 1px solid lightgray;
}
.inline {
margin: 5px 0;
display: flex;
align-items: center;
}
.inline > * {
margin-right: 5px;
}
.input {
width: 50px;
}
.preview-canvas {
border: 1px solid lightgray;
background-color: black;
}
.status {
width: 16px;
height: 16px;
border-radius: 10px;
margin-left: 5px;
}
</style>
import Vue from 'vue'
import App from './App.vue'
import store from './store'
Vue.config.productionTip = false
new Vue({
store,
render: h => h(App)
}).$mount('#app')
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
},
modules: {
}
})
module.exports = {
publicPath: '',
};
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment