前回、サンプルスケッチを改造して撮影、パラメータの設定が出来る様になりまいた。でもこのHP、見た目はサンプルスケッチと殆ど同じですが写真の処理が本来のサンプルスケッチと違っています。本来のサンプルスケッチは撮影した画像を直接クライアントに送信しているのに対し、改良HPは画像を一度SDカードに保存して、それを送っていました。つまりサンプルスケッチはSDカード無しに画像をクライアントに送れるのです。もう一つ大きな違いは、サンプルスケッチにはストリューム機能が有ることです。今回はTimeLapsを実装する予定でしたが、この辺の機能を実装することにしました。
先ずは、画像(静止画)の送信
改造したHPで写真を送信する場合、
- fb = esp_camera_fb_get(); 写真処理関係関数のポインタ取得
- dataFile.write(fb->buf, fb->len); SDカードに”data.jpg”で保存
- <img id=”stream” src=”./data.jpg”> クライアントからの画像要求に対し
- server.streamFile(dataFile, dataType); で保存された画像を送信
写真処理関係関数のポインタを取得した時点でバッファーとその大きさは分かっているのでこれらを使って送れないか検討してみました。
画像をクライアントに送信する場合、サーバの応答ヘッダーは、
HTTP/1.1 200 OK
Content-type: image/jpeg
Content-Length: xxxxxx
Connection: close
- 1行: クライアントへの応答。200は応答了解
- 2行: これから送る画像の形式。今回はJPG
- 3行: 画像データの長さ
- 4行: 送信後接続をクローズ
これらのパラメタを送信した後、画像データを送信する様です。
改良スケッチではデータをクライアント送信するのに、”sendContent()”を使用います。これを使って”fb-buf”のデータを送ろうとした所、データの型が違い”sendContent()”が使用出来ませんでした。そこで、新たに、”server()”関数から”client = server.client();”を作成。クライアントと繋ぎ、”client.write()”でクライアントにデータ送信する事にしました。この関数は、”client.write(fb->buf, fb->len);”の様にバッファーのポインターとその長さを引数にした関数です。
これらの実装場所ですが、”void handleWebRequests()”になります。この関数はクライアントからファイルの送信要求が有った場合実行される関数です。改良スケッチでは
void handleWebRequests()
{
String dataType = "text/plain";
String path;
File dataFile;
path = server.uri();
if(path.endsWith(".txt")) dataType = "text/plain";
else if(path.endsWith(".css")) dataType = "text/css";
else if(path.endsWith(".js")) dataType = "application/javascript";
else if(path.endsWith(".png")) dataType = "image/png";
else if(path.endsWith(".gif")) dataType = "image/gif";
else if(path.endsWith(".jpg")) dataType = "image/jpeg";
delay(5);
dataFile = SD.open(path.c_str(), "r");
server.streamFile(dataFile, dataType);
dataFile.close();
delay(5);
}
となっています。13行の、”else if(path.endsWith(“.jpg”)) dataType = “image/jpeg”;”を修正します。
void handleWebRequests()
{
String dataType = "text/plain";
String path;
File dataFile;
int a = 1;
WiFiClient client;
camera_fb_t * fb;
path = server.uri();
if(path.endsWith(".txt")) dataType = "text/plain";
else if(path.endsWith(".css")) dataType = "text/css";
else if(path.endsWith(".js")) dataType = "application/javascript";
else if(path.endsWith(".png")) dataType = "image/png";
else if(path.endsWith(".html")) dataType = "text/html";
else if(path.endsWith(".jpg"))
{
fb = esp_camera_fb_get();
client = server.client();
String response = "HTTP/1.1 200 OK\r\n";
response += "Content-type: image/jpeg\r\n";
response += ("Content-Length: " + String(fb->len) + "\r\n");
response += "Connection: close\r\n\r\n";
server.sendContent(response);
client.write(fb->buf, fb->len);
esp_camera_fb_return(fb);
a=0;
}
delay(5);
if(a)
{
dataFile = SD.open(path.c_str(), "r");
server.streamFile(dataFile, dataType);
dataFile.close();
delay(5);
}
- 6行: 操作フラグの宣言
- 7行: クライアント接続用インスタンスの宣言
- 8行: カメラ撮影用インスタンスの宣言
- 17から28行: 変更箇所
- 18行: 写真撮影
- 19行: クライアントに接続
- 20から24行: ヘッダーの送信
- 25行: 写真データの送信
- 26行: 写真関数後処理
- 27行: 操作フラグ=0
- 31行: ここでJPG以外は通常の処理を行う様に処理しています。
これで、SDに画像を保存すること無しに、かつHTMLの変更無しに動作しました。
次は、Stream
静止画が送れるのならそれを繰り返せば動画になるかもって思ったら、そうゆう方式が有りました。その方式をMotionJPEGと呼ぶ様です。こんな感じでデータを送信します。
HTTP/1.1 200 OK
Content-Type: multipart/x-mixed-replace; boundary=--frame
--frame
Content-Type: image/jpeg
client.write(fb->buf, fb->len);
- 1行: クライアントへの応答
- 2行:
- multipart/x-mixed-replace; 送信データがMotionJPEGと宣言
- boundary=–frame データの区切りに ”–frame”を使用すると宣言。クライアントは、”–frame”がサーバーから送られて来ると新しい画像データになったと判断し、画像の上書きをする様です。
- 3から4行: その後2行改行
- 5行: 画像データを送る前に先ず、データ区切り、”–frame”を送信
- 6行: データがJPEG方式である事を宣言
- 7行: 改行送信
- 8行: 画像データを送信
- 9行: 改行を送信後、5行に戻る。
これを元にコードを書くと、JPEGの時に拡張子を、”jpg”と指定した様に、Streamの時は拡張子を、”strm”と指定するとします。
else if(path.endsWith(".strm"))
{
client = server.client();
String response = "HTTP/1.1 200 OK\r\n";
response += "Content-Type: multipart/x-mixed-replace; boundary=--frame\r\n\r\n";
server.sendContent(response);
while (1)
{
response = "--frame\r\n";
response += "Content-Type: image/jpeg\r\n\r\n";
server.sendContent(response);
fb = esp_camera_fb_get();
client.write(fb->buf, fb->len);
server.sendContent("\r\n");
esp_camera_fb_return(fb);
}
}
- 1行: Streamの要求あり
- 4行: クライアントへの応答。最後の ”\r\n” は改行を意味します。
- 5行: ここでMotionJPEGを宣言しています。この行の最後は、”\r\n\r\n”となっていて、これは2行改行を意味します。
- 8行: ここからStream開始。
- 10行: 区切りデータを送信
- 11行: データタイプを送信
- 14行: 写真の撮影
- 15行: 画像データの送信
- 16行: 改行を送信後10行へ
今回はHTMLの変更も必要で、画像データの読み込みを指定している部分
<img id="stream" src="./data.jpg">
を
<img id="stream" src="./data.strm">
とします。これでStream再生が出来ます。
実際の実装では
実際の実装では下記が問題になりますが、以下の様に対応しています。
- HTMLでどうやって、画像読み込み部分<ing id=”stream” src=”./data.jpg”>を書き換えるのか。
- ScriptでHTMLが実行される前に問題の箇所を書き換える
- Streamの終了をどのようにするのか。
- ボタンが押されたら、
- クライアント:window.stop()を実行しサーバとの接続を切る
- サーバー:クライアントとの接続が切れたら、Streamデータの送信を止める。
- ボタンが押されたら、
全てのコード
先ずはスケッチ
#include "esp_camera.h"
#include "Arduino.h"
#include "SD.h"
#include <WiFi.h>
#include <WebServer.h>
#include <ESPmDNS.h>
// Pin definition for CAMERA_MODEL_AI_THINKER
#define PWDN 4
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
#define sd_sck 14
#define sd_mosi 15
#define sd_ss 13
#define sd_miso 32
WebServer server(80);
const char *SSID = "enter your SSID";
const char *PASSWORD = "enter your password";
RTC_DATA_ATTR int cam_state[25]={0};
#define ope_stat 24
camera_config_t config;
void set_conf()
{
// Init Camera
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sscb_sda = SIOD_GPIO_NUM;
config.pin_sscb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG;
config.frame_size = FRAMESIZE_VGA; // FRAMESIZE_ + QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA
config.jpeg_quality = 10;
config.fb_count = 2;
}
void setup() {
pinMode(PWDN, OUTPUT);
digitalWrite(PWDN, LOW);
Serial.begin(115200);
delay(100);
set_conf();
esp_camera_init(&config);
SPI.begin(sd_sck, sd_miso, sd_mosi, sd_ss);
SD.begin(sd_ss);
Serial.println("Connecting to WiFi");
WiFi.disconnect(true);
WiFi.softAPdisconnect(true);
delay(500);
WiFi.mode(WIFI_STA);
WiFi.begin(SSID, PASSWORD);
delay(1000);
// Try forever
while (WiFi.status() != WL_CONNECTED) {
Serial.println("...Connecting to WiFi");
delay(1000);
}
Serial.println("Connected");
Serial.println(SSID);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
if (MDNS.begin("esp32devcam")) {
Serial.println("MDNS responder started");
}
server.on("/", handleRoot);
server.onNotFound(handleWebRequests);
server.begin();
Serial.println("HTTP server started");
read_State();
cam_state[ope_stat] = 0;
}
void read_State()
{
sensor_t * s = esp_camera_sensor_get();
cam_state[0]= s->status.framesize;
cam_state[1]= s->status.quality;
cam_state[2]= s->status.brightness;
cam_state[3]= s->status.contrast;
cam_state[4]= s->status.saturation;
cam_state[5]= s->status.sharpness;
cam_state[6]= s->status.special_effect;
cam_state[7]= s->status.awb;
cam_state[8]= s->status.awb_gain;
cam_state[9]= s->status.wb_mode;
cam_state[10]= s->status.aec;
cam_state[11]= s->status.aec2;
cam_state[12]= s->status.ae_level;
cam_state[13]= s->status.agc;
cam_state[14]= s->status.agc_gain;
cam_state[15]= s->status.gainceiling;
cam_state[16]= s->status.bpc;
cam_state[17]= s->status.wpc;
cam_state[18]= s->status.raw_gma;
cam_state[19]= s->status.lenc;
cam_state[20]= s->status.hmirror;
cam_state[21]= s->status.vflip;
cam_state[22]= s->status.dcw;
cam_state[23]= s->status.colorbar;
}
void loop() {
server.handleClient();
}
void handleRoot() {
String buf,cmd;
int a,b,fl,c_state[24];
File dataFile;
camera_fb_t * fb = NULL;
sensor_t * s;
fl=1;
cmd=server.argName(0);
switch(cmd.toInt())
{
case 1: // Get Still
cam_state[ope_stat] = 2;
break;
case 2: //Set Camera Parameter
buf=server.arg("2");
a=buf.toInt();
if(a < 0)
{
a = -a; a -= 10;
cam_state[a] ^= 1; cam_state[a] &= 1;
}
else
{
a -= 10;
b=0; while(buf[b] != ',') b ++;
b ++;
cmd=buf.substring(b);
cam_state[a]=cmd.toInt();
}
s = esp_camera_sensor_get();
switch(a)
{
case 0: s->set_framesize(s,(framesize_t)cam_state[0]); break; //framesize
case 1: s->set_quality(s,cam_state[1]); break; //quality
case 2: s->set_brightness(s,cam_state[2]); break; //brightness
case 3: s->set_contrast(s,cam_state[3]); break; //contrast
case 4: s->set_saturation(s,cam_state[4]); break; //saturation
case 5: s->set_sharpness(s,cam_state[5]); break; //sharpness
case 6: s->set_special_effect(s,cam_state[6]); break; //special_effect
case 7: s->set_whitebal(s,cam_state[7]); break; //awb
case 8: s->set_awb_gain(s,cam_state[8]); break; //awb_gain
case 9: s->set_wb_mode(s,cam_state[9]); break; //wb_mode
case 10: s->set_aec2(s,cam_state[10]); break; //aec
case 11: s->set_exposure_ctrl(s,cam_state[11]); break; //aec2
case 12: s->set_ae_level(s,cam_state[12]); break; //ae_level
case 13: s->set_gain_ctrl(s,cam_state[13]); break; //agc
case 14: s->set_agc_gain(s,cam_state[14]); break; //agc_gain
case 15: s->set_gainceiling(s,(gainceiling_t)cam_state[15]); break; //gainceiling
case 16: s->set_bpc(s,cam_state[16]); break; //bpc
case 17: s->set_wpc(s,cam_state[17]); break; //wpc
case 18: s->set_raw_gma(s,cam_state[18]); break; //raw_gma
case 19: s->set_lenc(s,cam_state[19]); break; //lenc
case 20: s->set_hmirror(s,cam_state[20]); break; //hmirror
case 21: s->set_vflip(s,cam_state[21]); break; //vflip
case 22: s->set_dcw(s,cam_state[22]); break; //dcw
case 23: s->set_colorbar(s,cam_state[23]); break; //colorba
}
fl=0;
server.send(204, "text/plain", "");
break;
case 3: // Send camera parameter
cmd="";
for(a=0; a<25; a++) cmd += (String(cam_state[a]) + ',');
server.send(200, "text/plain", cmd);
fl=0;
break;
case 4: // Stream Start Stop
if(cam_state[ope_stat])
{
cam_state[ope_stat] = 0;
fl=0;
server.send(204, "text/plain", "");
}
else cam_state[ope_stat] = 1;
break;
}
if(fl)
{
dataFile = SD.open("/menu.html", FILE_READ);
server.streamFile(dataFile,"text/html");
dataFile.close();
}
}
void handleWebRequests()
{
String dataType = "text/plain";
String path;
File dataFile;
int a = 1;
WiFiClient client;
camera_fb_t * fb;
path = server.uri();
if(path.endsWith(".txt")) dataType = "text/plain";
else if(path.endsWith(".jpg"))
{
fb = esp_camera_fb_get();
client = server.client();
String response = "HTTP/1.1 200 OK\r\n";
response += "Content-type: image/jpeg\r\n";
response += ("Content-Length: " + String(fb->len) + "\r\n");
response += "Connection: close\r\n\r\n";
server.sendContent(response);
client.write(fb->buf, fb->len);
esp_camera_fb_return(fb);
cam_state[ope_stat] = 0;
a=0;
}
else if(path.endsWith(".strm"))
{
client = server.client();
String response = "HTTP/1.1 200 OK\r\n";
response += "Content-Type: multipart/x-mixed-replace; boundary=--frame\r\n\r\n";
server.sendContent(response);
while (1)
{
fb = esp_camera_fb_get();
if (!client.connected())
{
esp_camera_fb_return(fb);
break;
}
response = "--frame\r\n";
response += "Content-Type: image/jpeg\r\n\r\n";
server.sendContent(response);
client.write(fb->buf, fb->len);
server.sendContent("\r\n");
esp_camera_fb_return(fb);
if (!client.connected()) break;
}
a=0;
}
else if(path.endsWith(".css")) dataType = "text/css";
else if(path.endsWith(".js")) dataType = "application/javascript";
else if(path.endsWith(".png")) dataType = "image/png";
else if(path.endsWith(".html")) dataType = "text/html";
delay(5);
if(a)
{
dataFile = SD.open(path.c_str(), "r");
server.streamFile(dataFile, dataType);
dataFile.close();
delay(5);
}
}
- 278,292行: ここでクライアントとの接続を確認しています。
- client.connected(): クライアントとの接続状態を確認する関数。クライアントとの接続が切れた場合、戻り値が”0”となります。
- 写真データを送る前(278行)と送った後(292行)にクライアントとの接続を確認しています。
以下は、Scriptファイル。ファイル名は、”menu.js”です。SDカードのRootに保存して下さい。
menu.js
var para[24]={"0"};
document.addEventListener('change', function (event) {
var targetElement = event.target || event.srcElement;
document.getElementById('123').value=targetElement.id + ',' + targetElement.value;
document.getElementById('abc').submit();
}
,false);
document.addEventListener('DOMContentLoaded', function (event) {
//function get_data(){
var url = "http://esp32devcam.local?3=";
var xhr = new XMLHttpRequest();
var a,b,str;
xhr.open('GET', url);
xhr.send();
xhr.onreadystatechange = function()
{
if(xhr.readyState === 4 && xhr.status === 200)
{
console.log( xhr.responseText );
b=0;
for(a=0; a<25; a++)
{
para[a]='';
while( xhr.responseText[b] != ',')
{
para[a] += xhr.responseText[b];
b ++;
}
b ++;
}
for(a=0; a<24; a++)
{
switch(a)
{
case 0:
case 6:
case 9: document.getElementById(String(a + 10)).selectedIndex=Number(para[a]);
break;
case 1:
case 2:
case 3:
case 4:
case 5:
case 12:
case 14:
case 15: document.getElementById(String(a + 10)).value = para[a];
break;
default: str=true;
if(para[a] == "0") str=false;
document.getElementById(String(-a - 10)).checked = str;
break;
}
}
switch(Number(para[24]))
{
case 0:
document.getElementById("stream").src = ``;
document.getElementById('st_start').innerHTML = "Start Stream";
able_disable(1);
break;
case 1:
document.getElementById("stream").src = `./data.strm`;
document.getElementById('st_start').innerHTML = "Stop Stream";
able_disable(0);
break;
case 2:
document.getElementById("stream").src = `./data.jpg`;
break;
}
}
}
});
function onBtnStearm() {
if(Number(para[24]) == 1)
{
window.stop();
document.getElementById('st_start').innerHTML = "Start Stream";
para[24]="0";
able_disable(1);
}
}
function able_disable(flg)
{
// flg=0: disable flg=1: able
var a;
var str=true;
var disp=['-17','-18','19','-20','-21','22','-23','24','25'];
if(flg) str=false;
for(a=10; a<17; a++)
document.getElementById(String(a)).disabled = str;
for(a=0; a<9; a++)
document.getElementById(disp[a]).disabled = str;
for(a=-33; a<-25; a++)
document.getElementById(String(a)).disabled = str;
document.getElementById("get-still").disabled = str;
}
history.pushState(null,null,'/');
- 62から77行:この部分でHTMLを書き換えています。
- 92から90行:Steeam用ボタンの表示と接続を切る関数。
以下は、HTMLソース。ファイル名は、”menu.html”です。SDカードのRootに保存して下さい。
menu.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<script type='text/javascript' src='menu.js'></script>
<link rel='stylesheet' type='text/css' href='menu.css' >
<title>ESP32 OV2460</title>
</head>
<body>
<section class="main">
<div style='font-size:40px'><b><i><u>Timelapse</u></i></b><br></div>
<div id="content">
<div id="sidebar">
<nav id="menu">
<div class="input-group">
<label>Resolution</label>
<select id="10" >
<option value='0' style="display:none">QQVGA(160x120)</option>
<option value='1'>QQVGA(160x120)</option>
<option value='2'>QCIF(176x144)</option>
<option value='3'>HQVGA(240x176)</option>
<option value='4'>QVGA(320x240)</option>
<option value='5'>CIF(400x296)</option>
<option value='6' selected >VGA(640x480)</option>
<option value='7'>SVGA(800x600)</option>
<option value='8'>XGA(1024x768)</option>
<option value='9'>SXGA(1280x1024)</option>
<option value='10'>UXGA(1600x1200)</option>
</select>
</div>
<div class="input-group">
<label>Quality</label>
<div class="range-min">10</div>
<input type="range" id="11" min="10" max="63" value="10" >
<div class="range-max">63</div>
</div>
<div class="input-group">
<label>Brightness</label>
<div class="range-min">-2</div>
<input type="range" id="12" min="-2" max="2" value="0">
<div class="range-max">2</div>
</div>
<div class="input-group">
<label>Contrast</label>
<div class="range-min">-2</div>
<input type="range" id="13" min="-2" max="2" value="0">
<div class="range-max">2</div>
</div>
<div class="input-group">
<label>Saturation</label>
<div class="range-min">-2</div>
<input type="range" id="14" min="-2" max="2" value="0" >
<div class="range-max">2</div>
</div>
<div class="input-group">
<label>Sharpness</label>
<div class="range-min">-2</div>
<input type="range" id="15" min="-2" max="2" value="0" >
<div class="range-max">2</div>
</div>
<div class="input-group">
<label>Special Effect</label>
<select id="16" >
<option value="0" selected="selected">No Effect</option>
<option value="1">Negative</option>
<option value="2">Grayscale</option>
<option value="3">Red Tint</option>
<option value="4">Green Tint</option>
<option value="5">Blue Tint</option>
<option value="6">Sepia</option>
</select>
</div>
<div class="input-group">
<label for="-17">AWB</label>
<div class="switch">
<input id="-17" type="checkbox">
<label class="slider" for="-17"></label>
</div>
</div>
<div class="input-group">
<label for="-18">AWB Gain</label>
<div class="switch">
<input id="-18" type="checkbox">
<label class="slider" for="-18"></label>
</div>
</div>
<div class="input-group">
<label>WB Mode</label>
<select id="19" >
<option value="0" selected="selected">Auto</option>
<option value="1">Sunny</option>
<option value="2">Cloudy</option>
<option value="3">Office</option>
<option value="4">Home</option>
</select>
</div>
<div class="input-group">
<label for="-20">AEC SENSOR</label>
<div class="switch">
<input id="-20" type="checkbox">
<label class="slider" for="-20"></label>
</div>
</div>
<div class="input-group">
<label for="-21">AEC DSP</label>
<div class="switch">
<input id="-21" type="checkbox" >
<label class="slider" for="-21"></label>
</div>
</div>
<div class="input-group">
<label>AE Level</label>
<div class="range-min">-2</div>
<input type="range" id="22" min="-2" max="2" value="0">
<div class="range-max">2</div>
</div>
<div class="input-group">
<label for="-23">AGC</label>
<div class="switch">
<input id="-23" type="checkbox" >
<label class="slider" for="-23"></label>
</div>
</div>
<div class="input-group">
<label>Agc_gain</label>
<div class="range-min">0</div>
<input type="range" id="24" min="0" max="30" value="0">
<div class="range-max">30</div>
</div>
<div class="input-group">
<label>Gain Ceiling</label>
<div class="range-min">2x</div>
<input type="range" id="25" min="0" max="6" value="0">
<div class="range-max">128x</div>
</div>
<div class="input-group">
<label for="-26">BPC</label>
<div class="switch">
<input id="-26" type="checkbox" >
<label class="slider" for="-26"></label>
</div>
</div>
<div class="input-group">
<label for="-27">WPC</label>
<div class="switch">
<input id="-27" type="checkbox">
<label class="slider" for="-27"></label>
</div>
</div>
<div class="input-group">
<label for="-28">Raw GMA</label>
<div class="switch">
<input id="-28" type="checkbox">
<label class="slider" for="-28"></label>
</div>
</div>
<div class="input-group">
<label for="-29">Lens Correction</label>
<div class="switch">
<input id="-29" type="checkbox">
<label class="slider" for="-29"></label>
</div>
</div>
<div class="input-group">
<label for="-30">H-Mirror</label>
<div class="switch">
<input id="-30" type="checkbox">
<label class="slider" for="-30"></label>
</div>
</div>
<div class="input-group">
<label for="-31">V-Flip</label>
<div class="switch">
<input id="-31" type="checkbox">
<label class="slider" for="-31"></label>
</div>
</div>
<div class="input-group">
<label for="-32">DCW (Downsize EN)</label>
<div class="switch">
<input id="-32" type="checkbox">
<label class="slider" for="-32"></label>
</div>
</div>
<div class="input-group">
<label for="-33">Color Bar</label>
<div class="switch">
<input id="-33" type="checkbox" >
<label class="slider" for="-33"></label>
</div>
</div>
<form method='get' id='abc'>
<input name="2" id='123' style="display:none">
</form>
<form method='get'>
<sp>
<button type='submit' name='1' id="get-still" style='float:left'>Get Still</button>
<button type='submit' name='4' id="st_start" style='float:right' onClick="onBtnStearm()" >Start Stream</button>
</sp>
</form>
</nav>
</div>
<figure>
<img id="stream" src="">
</figure>
</div>
</section>
</body>
</html>
前回との違いは、ResetボタンをStreamボタンに変更のみです。(199行)
最後にCSSファイル。ファイル名、”menu.css”でRootに保存して下さい。これは前回と同じです。
menu.css
@charset "UTF-8";
body {
font-family: Arial,Helvetica,sans-serif;
background: #181818;
color: #EFEFEF;
font-size: 16px
}
section.main {
display: flex
}
#menu,section.main {
flex-direction: column
}
#menu {
display: flex;
flex-wrap: nowrap;
min-width: 340px;
background: #363636;
padding: 8px;
border-radius: 4px;
margin-top: -10px;
margin-right: 10px;
}
#content {
display: flex;
flex-wrap: wrap;
align-items: stretch
}
.input-group {
display: flex;
flex-wrap: nowrap;
line-height: 22px;
margin: 5px 0
}
.input-group>label {
display: inline-block;
padding-right: 10px;
min-width: 47%
}
.input-group input,.input-group select {
flex-grow: 1
}
.range-max,.range-min {
display: inline-block;
padding: 0 5px
}
button {
display: block;
margin: 5px;
padding: 0 12px;
border: 0;
line-height: 28px;
cursor: pointer;
color: #fff;
background: #ff3034;
border-radius: 5px;
font-size: 16px;
outline: 0
}
input[type=range] {
-webkit-appearance: none;
width: 100%;
height: 22px;
background: #363636;
cursor: pointer;
margin: 0
}
input[type=range]::-moz-range-track {
width: 100%;
height: 2px;
cursor: pointer;
background: #EFEFEF;
border-radius: 0;
border: 0 solid #EFEFEF
}
input[type=range]::-moz-range-thumb {
border: 1px solid rgba(0,0,30,0);
height: 22px;
width: 22px;
border-radius: 50px;
background: #ff3034;
cursor: pointer
}
input:checked+.slider {
background-color: #ff3034
}
input:checked+.slider:before {
-webkit-transform: translateX(26px);
transform: translateX(26px)
}
.switch {
display: block;
position: relative;
line-height: 22px;
font-size: 16px;
height: 22px
}
.switch input {
outline: 0;
opacity: 0;
width: 0;
height: 0
}
.slider {
width: 50px;
height: 22px;
border-radius: 22px;
cursor: pointer;
background-color: grey
}
.slider,.slider:before {
display: inline-block;
transition: .4s
}
.slider:before {
position: relative;
content: "";
border-radius: 50%;
height: 16px;
width: 16px;
left: 4px;
top: 3px;
background-color: #fff
}
select {
border: 1px solid #363636;
font-size: 14px;
height: 22px;
outline: 0;
border-radius: 5px
}
figure {
padding: 0px;
margin: 0;
-webkit-margin-before: 0;
margin-block-start: 0;
-webkit-margin-after: 0;
margin-block-end: 0;
-webkit-margin-start: 0;
margin-inline-start: 0;
-webkit-margin-end: 0;
margin-inline-end: 0
}
figure img {
display: block;
width: 100%;
height: auto;
border-radius: 4px;
margin-top: 8px;
}
.image-container {
position: relative;
min-width: 160px
}
.hidden {
display: none
}
.close {
position: absolute;
right: 5px;
top: 5px;
background: #ff3034;
width: 16px;
height: 16px;
border-radius: 100px;
color: #fff;
text-align: center;
line-height: 18px;
cursor: pointer
}
input.ip1 {width:30px;}
input.ip2 {width:20px;}
コンパイル後実行して下さい。コンピュター名,”eps32devcam”でアクセス出来る様にしていますので、Webブラウザに、”eps32devcam.local”と入力して下さい。

画面下にある2つのボタンで、写真とStreamが撮れます。これでかなりサンプルスケッチに近くなりました。次回は今度こそ、”TimeLaps”の実装です。