diff --git a/RemoteCamera/README.md b/RemoteCamera/README.md
new file mode 100644
index 0000000..a44b9db
--- /dev/null
+++ b/RemoteCamera/README.md
@@ -0,0 +1,6 @@
+# RemoteCamera
+
+Connect to a V4L2 camera on server via Antunnel.
+
+This application reauires the **tunel plugin** and the **ant-tunnel v4l2 publisher**
+on the server-side
\ No newline at end of file
diff --git a/RemoteCamera/assets/scheme.html b/RemoteCamera/assets/scheme.html
new file mode 100644
index 0000000..04bc064
--- /dev/null
+++ b/RemoteCamera/assets/scheme.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RemoteCamera/build/debug/README.md b/RemoteCamera/build/debug/README.md
new file mode 100644
index 0000000..a44b9db
--- /dev/null
+++ b/RemoteCamera/build/debug/README.md
@@ -0,0 +1,6 @@
+# RemoteCamera
+
+Connect to a V4L2 camera on server via Antunnel.
+
+This application reauires the **tunel plugin** and the **ant-tunnel v4l2 publisher**
+on the server-side
\ No newline at end of file
diff --git a/RemoteCamera/build/debug/main.css b/RemoteCamera/build/debug/main.css
new file mode 100644
index 0000000..0f48adf
--- /dev/null
+++ b/RemoteCamera/build/debug/main.css
@@ -0,0 +1,12 @@
+
+afx-app-window[data-id="RemoteCamera"] div[data-id="container"]
+{
+ display: block;
+ overflow: auto;
+}
+
+afx-app-window[data-id="RemoteCamera"] div[data-id="container"] canvas
+{
+ display: block;
+ margin:0 auto;
+}
\ No newline at end of file
diff --git a/RemoteCamera/build/debug/main.js b/RemoteCamera/build/debug/main.js
new file mode 100644
index 0000000..a6d2829
--- /dev/null
+++ b/RemoteCamera/build/debug/main.js
@@ -0,0 +1,221 @@
+(function() {
+ var RemoteCamera;
+
+ RemoteCamera = class RemoteCamera extends this.OS.application.BaseApplication {
+ constructor(args) {
+ super("RemoteCamera", args);
+ }
+
+ main() {
+ var fps, i, j;
+ this.mute = false;
+ this.player = this.find("player");
+ this.qctl = this.find("qctl");
+ this.fpsctl = this.find("fpsctl");
+ this.cam_setting = {
+ w: 640,
+ h: 480,
+ fps: 10,
+ quality: 60
+ };
+ fps = [];
+ for (i = j = 5; j <= 30; i = j += 5) {
+ fps.push({
+ text: `${i}`,
+ value: i
+ });
+ }
+ this.fpsctl.data = fps;
+ this.fpsctl.selected = this.cam_setting.fps / 5 - 1;
+ this.fpsctl.onlistselect = (e) => {
+ if (this.mute) {
+ return;
+ }
+ this.cam_setting.fps = e.data.item.data.value;
+ return this.setCameraSetting();
+ };
+ this.qctl.value = this.cam_setting.quality;
+ this.resoctl = this.find("resoctl");
+ this.resoctl.data = [
+ {
+ text: __("320x240"),
+ mode: "qvga"
+ },
+ {
+ text: __("640x480"),
+ selected: true,
+ mode: "vga"
+ },
+ {
+ text: __("800x600"),
+ mode: "svga"
+ },
+ {
+ text: __("1024x760"),
+ mode: "hd"
+ },
+ {
+ text: __("1920×1080"),
+ mode: "fhd"
+ }
+ ];
+ this.resoctl.onlistselect = (e) => {
+ if (this.mute) {
+ return;
+ }
+ switch (e.data.item.data.mode) {
+ case "qvga":
+ this.cam_setting.w = 320;
+ this.cam_setting.h = 240;
+ break;
+ case "vga":
+ this.cam_setting.w = 640;
+ this.cam_setting.h = 480;
+ break;
+ case "svga":
+ this.cam_setting.w = 800;
+ this.cam_setting.h = 600;
+ break;
+ case "hd":
+ this.cam_setting.w = 1024;
+ this.cam_setting.h = 768;
+ break;
+ case "fhd":
+ this.cam_setting.w = 1920;
+ this.cam_setting.h = 1080;
+ }
+ return this.setCameraSetting();
+ };
+ this.qctl.onvaluechange = (e) => {
+ if (this.mute) {
+ return;
+ }
+ this.cam_setting.quality = e.data;
+ return this.setCameraSetting();
+ };
+ if (!Antunnel.tunnel) {
+ return this.notify(__("Antunnel service is not available"));
+ }
+ if (!this.setting.channel) {
+ return this.requestChannel();
+ } else {
+ return this.openSession();
+ }
+ }
+
+ requestChannel() {
+ return this.openDialog("PromptDialog", {
+ title: __("Enter camera channel"),
+ label: __("Please enter camera channel name")
+ }).then((v) => {
+ this.setting.channel = v;
+ return this.openSession();
+ });
+ }
+
+ menu() {
+ return {
+ text: "__(Option)",
+ nodes: [
+ {
+ text: "__(Camera channel)"
+ }
+ ],
+ onchildselect: (e) => {
+ return this.requestChannel();
+ }
+ };
+ }
+
+ openSession() {
+ if (!Antunnel) {
+ return;
+ }
+ if (!this.setting.channel) {
+ return;
+ }
+ this.tunnel = Antunnel.tunnel;
+ this.sub = new Antunnel.Subscriber(this.setting.channel);
+ this.sub.onopen = () => {
+ return console.log("Subscribed to camera channel");
+ };
+ this.sub.onerror = (e) => {
+ return this.error(__("Error: {0}", new TextDecoder("utf-8").decode(e.data)), e);
+ };
+ //@sub = undefined
+ this.sub.onctrl = (e) => {
+ var res;
+ this.cam_setting.w = Antunnel.Msg.int_from(e.data, 0);
+ this.cam_setting.h = Antunnel.Msg.int_from(e.data, 2);
+ this.cam_setting.fps = e.data[4];
+ this.cam_setting.quality = e.data[5];
+ this.mute = true;
+ this.qctl.value = this.cam_setting.quality;
+ res = `${this.cam_setting.w}x${this.cam_setting.h}`;
+ switch (res) {
+ case "320x240":
+ this.resoctl.selected = 0;
+ break;
+ case "640x480":
+ this.resoctl.selected = 1;
+ break;
+ case "800x600":
+ this.resoctl.selected = 2;
+ break;
+ case "1024x768":
+ this.resoctl.selected = 3;
+ break;
+ case "1920x1080":
+ this.resoctl.selected = 4;
+ }
+ this.fpsctl.selected = this.cam_setting.fps / 5 - 1;
+ return this.mute = false;
+ };
+ this.sub.onmessage = (e) => {
+ var context, imgData, jpeg;
+ jpeg = new JpegImage();
+ jpeg.parse(e.data);
+ context = this.player.getContext("2d");
+ this.player.width = jpeg.width;
+ this.player.height = jpeg.height;
+ //jpeg.copyToImageData(d)
+ imgData = context.getImageData(0, 0, jpeg.width, jpeg.height);
+ jpeg.copyToImageData(imgData);
+ return context.putImageData(imgData, 0, 0);
+ };
+ this.sub.onclose = () => {
+ this.sub = void 0;
+ this.notify(__("Unsubscribed to the camera service"));
+ return this.quit();
+ };
+ return Antunnel.tunnel.subscribe(this.sub);
+ }
+
+ cleanup() {
+ if (this.sub) {
+ return this.sub.close();
+ }
+ }
+
+ setCameraSetting() {
+ var arr;
+ if (!this.sub) {
+ return;
+ }
+ arr = new Uint8Array(6);
+ arr.set(Antunnel.Msg.bytes_of(this.cam_setting.w), 0);
+ arr.set(Antunnel.Msg.bytes_of(this.cam_setting.h), 2);
+ arr[4] = this.cam_setting.fps;
+ arr[5] = this.cam_setting.quality;
+ return this.sub.send(Antunnel.Msg.CTRL, arr);
+ }
+
+ };
+
+ RemoteCamera.singleton = true;
+
+ RemoteCamera.dependencies = ["pkg://libjpeg/jpg.js"];
+
+ this.OS.register("RemoteCamera", RemoteCamera);
+
+}).call(this);
diff --git a/RemoteCamera/build/debug/package.json b/RemoteCamera/build/debug/package.json
new file mode 100644
index 0000000..406327f
--- /dev/null
+++ b/RemoteCamera/build/debug/package.json
@@ -0,0 +1,16 @@
+{
+ "pkgname": "RemoteCamera",
+ "app":"RemoteCamera",
+ "name":"Remote Camera",
+ "description":"Connect to remote camera via Antunnel",
+ "info":{
+ "author": "",
+ "email": ""
+ },
+ "version":"0.0.1-a",
+ "category":"Other",
+ "iconclass":"fa fa-camera",
+ "mimes":["none"],
+ "dependencies":["libjpeg@0.1.1-a", "Antunnel@0.1.8-a"],
+ "locale": {}
+}
\ No newline at end of file
diff --git a/RemoteCamera/build/debug/scheme.html b/RemoteCamera/build/debug/scheme.html
new file mode 100644
index 0000000..04bc064
--- /dev/null
+++ b/RemoteCamera/build/debug/scheme.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/RemoteCamera/coffees/main.coffee b/RemoteCamera/coffees/main.coffee
new file mode 100644
index 0000000..4fcc9ae
--- /dev/null
+++ b/RemoteCamera/coffees/main.coffee
@@ -0,0 +1,177 @@
+class RemoteCamera extends this.OS.application.BaseApplication
+ constructor: ( args ) ->
+ super "RemoteCamera", args
+
+ main: () ->
+ @mute = false
+ @player = @find "player"
+
+ @qctl = @find "qctl"
+
+ @fpsctl = @find "fpsctl"
+
+ @cam_setting = {
+ w: 640,
+ h: 480,
+ fps: 10,
+ quality: 60
+ }
+
+ fps = []
+ for i in [5..30] by 5
+ fps.push {
+ text: "#{i}",
+ value: i
+ }
+ @fpsctl.data = fps
+ @fpsctl.selected = @cam_setting.fps/5 -1
+
+ @fpsctl.onlistselect = (e) =>
+ return if @mute
+ @cam_setting.fps = e.data.item.data.value
+ @setCameraSetting()
+
+ @qctl.value = @cam_setting.quality
+
+
+ @resoctl = @find "resoctl"
+
+ @resoctl.data = [
+ {
+ text: __("320x240"),
+ mode: "qvga"
+ },
+ {
+ text: __("640x480"),
+ selected: true,
+ mode: "vga"
+ },
+ {
+ text: __("800x600"),
+ mode: "svga"
+ },
+ {
+ text: __("1024x760"),
+ mode: "hd"
+ },
+ {
+ text: __("1920×1080"),
+ mode: "fhd"
+ }
+ ]
+ @resoctl.onlistselect = (e) =>
+ return if @mute
+ switch e.data.item.data.mode
+ when "qvga"
+ @cam_setting.w = 320
+ @cam_setting.h = 240
+ when "vga"
+ @cam_setting.w = 640
+ @cam_setting.h = 480
+ when "svga"
+ @cam_setting.w = 800
+ @cam_setting.h = 600
+ when "hd"
+ @cam_setting.w = 1024
+ @cam_setting.h = 768
+ when "fhd"
+ @cam_setting.w = 1920
+ @cam_setting.h = 1080
+ @setCameraSetting()
+
+ @qctl.onvaluechange = (e) =>
+ return if @mute
+ @cam_setting.quality = e.data
+ @setCameraSetting()
+
+ return @notify __("Antunnel service is not available") unless Antunnel.tunnel
+ if not @setting.channel
+ @requestChannel()
+ else
+ @openSession()
+
+ requestChannel: () ->
+ @openDialog "PromptDialog", {
+ title: __("Enter camera channel"),
+ label: __("Please enter camera channel name")
+ }
+ .then (v) =>
+ @setting.channel = v
+ @openSession()
+
+ menu: () ->
+ {
+ text: "__(Option)",
+ nodes: [
+ { text: "__(Camera channel)" }
+ ],
+ onchildselect: (e) => @requestChannel()
+ }
+
+ openSession: () ->
+ return unless Antunnel
+ return unless @setting.channel
+ @tunnel = Antunnel.tunnel
+ @sub = new Antunnel.Subscriber(@setting.channel)
+ @sub.onopen = () =>
+ console.log("Subscribed to camera channel")
+
+ @sub.onerror = (e) =>
+ @error __("Error: {0}", new TextDecoder("utf-8").decode(e.data)), e
+ #@sub = undefined
+ @sub.onctrl = (e) =>
+ @cam_setting.w = Antunnel.Msg.int_from(e.data,0)
+ @cam_setting.h = Antunnel.Msg.int_from(e.data,2)
+ @cam_setting.fps = e.data[4]
+ @cam_setting.quality = e.data[5]
+ @mute = true
+ @qctl.value = @cam_setting.quality
+ res = "#{@cam_setting.w}x#{@cam_setting.h}"
+ switch res
+ when "320x240"
+ @resoctl.selected = 0
+ when "640x480"
+ @resoctl.selected = 1
+ when "800x600"
+ @resoctl.selected = 2
+ when "1024x768"
+ @resoctl.selected = 3
+ when "1920x1080"
+ @resoctl.selected = 4
+ @fpsctl.selected = @cam_setting.fps/5 -1
+ @mute = false
+
+ @sub.onmessage = (e) =>
+ jpeg = new JpegImage()
+ jpeg.parse e.data
+ context = @player.getContext("2d")
+ @player.width = jpeg.width
+ @player.height = jpeg.height
+ #jpeg.copyToImageData(d)
+ imgData = context.getImageData(0,0,jpeg.width,jpeg.height)
+ jpeg.copyToImageData imgData
+ context.putImageData(imgData, 0, 0)
+
+ @sub.onclose = () =>
+ @sub = undefined
+ @notify __("Unsubscribed to the camera service")
+ @quit()
+ Antunnel.tunnel.subscribe @sub
+
+ cleanup: () ->
+ @sub.close() if @sub
+
+ setCameraSetting: () ->
+ return unless @sub
+ arr = new Uint8Array(6)
+ arr.set Antunnel.Msg.bytes_of(@cam_setting.w), 0
+ arr.set Antunnel.Msg.bytes_of(@cam_setting.h), 2
+ arr[4] = @cam_setting.fps
+ arr[5] = @cam_setting.quality
+ @sub.send Antunnel.Msg.CTRL, arr
+
+RemoteCamera.singleton = true
+RemoteCamera.dependencies = [
+ "pkg://libjpeg/jpg.js"
+]
+this.OS.register "RemoteCamera", RemoteCamera
\ No newline at end of file
diff --git a/RemoteCamera/css/main.css b/RemoteCamera/css/main.css
new file mode 100644
index 0000000..0b1854a
--- /dev/null
+++ b/RemoteCamera/css/main.css
@@ -0,0 +1,11 @@
+afx-app-window[data-id="RemoteCamera"] div[data-id="container"]
+{
+ display: block;
+ overflow: auto;
+}
+
+afx-app-window[data-id="RemoteCamera"] div[data-id="container"] canvas
+{
+ display: block;
+ margin:0 auto;
+}
\ No newline at end of file
diff --git a/RemoteCamera/package.json b/RemoteCamera/package.json
new file mode 100644
index 0000000..406327f
--- /dev/null
+++ b/RemoteCamera/package.json
@@ -0,0 +1,16 @@
+{
+ "pkgname": "RemoteCamera",
+ "app":"RemoteCamera",
+ "name":"Remote Camera",
+ "description":"Connect to remote camera via Antunnel",
+ "info":{
+ "author": "",
+ "email": ""
+ },
+ "version":"0.0.1-a",
+ "category":"Other",
+ "iconclass":"fa fa-camera",
+ "mimes":["none"],
+ "dependencies":["libjpeg@0.1.1-a", "Antunnel@0.1.8-a"],
+ "locale": {}
+}
\ No newline at end of file
diff --git a/RemoteCamera/project.json b/RemoteCamera/project.json
new file mode 100644
index 0000000..54687c9
--- /dev/null
+++ b/RemoteCamera/project.json
@@ -0,0 +1,7 @@
+{
+ "name": "RemoteCamera",
+ "css": ["css/main.css"],
+ "javascripts": [],
+ "coffees": ["coffees/main.coffee"],
+ "copies": ["assets/scheme.html", "package.json", "README.md"]
+}
\ No newline at end of file