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 diff is collapsed.
This diff is collapsed.
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