[Work/Class/ES2015で動的Webアプリ/WebSocketを用いた動的Webアプリ]

ES2015でWebSocket - TornadoでSecure WebSocket / 2017-12-07 (木)

https://で接続するときにはwss://

現在ChromeでセキュアHTTP(https://)接続をしようとすると,WebSocketも強制的にSecure WebSocketになるので,その対応.

Tornado側の対応

セキュア接続に必要な証明書と鍵(.crtファイルと.keyファイル)はサーバ毎に用意されている(取得しておく)もので,そのコピーを持って来て読み込める場所に設置しておき,Tornadoのサーバ生成を定義する段階,つまりtornado.web.Application()関数を実行してapplicationインスタンスを作り,それを回し始める時にssl_optionで読み込ませる.

ただし,外からhttpで見えるフォルダ,例えばクライアントhtml,jsファイルと同じフォルダに置いておくと,セキュリティ上問題があるので,外からは見えない(例えばpublic_htmlの上の階層など)フォルダに置いて置いて,そこから読み込む必要がある.

サーバ側のコードが今までの非セキュアなWebSocketとはちょっと違うことに注意.

インスタンスを生成するために証明書と鍵(certファイルとkeyファイル)を読み込む必要があり,前項のようなtornado.web.Application()関数に全部突っ込んでインスタンスを生成するための書法が2017年11月現在まだ完成していないため,暫定的にこうなっていると思われる.

そのため事前にapplicationインスタンスを作っておく必要がある.

(WebSocketサービスだけなら今までのようにtonado.web.Applicationクラスのインスタンスで回せていたが,Secureになると,applicationを生成しておいてから,それをtornado.httpserver.HTTPServerクラスのインスタンスを使って回す形になる)

コード例

# -*- coding:utf-8 -*-
import tornado.ioloop
import tornado.web
import tornado.websocket
import tornado.template
import tornado.httpserver

server_client_list = []
class SecureWSServer(tornado.websocket.WebSocketHandler):
    def check_origin(self, origin):
        return True

    def open(self):
        server_client_list.append(self)
        print("New Client Connected.")

    def on_message(self, message):
        print("Received message:" + message)
        for a_server_client in server_client_list:
            a_server_client.write_message(message)

    def on_close(self):
        if self in server_client_list:
            server_client_list.remove(self)
            print("Client Connection Closed")

application = tornado.web.Application(\
    [(r'/ws_secure_connect', SecureWSServer)])

if __name__ == "__main__":
    http_server = tornado.httpserver.HTTPServer(\
        application, ssl_options={
            "certfile": "mycert.crt",\
            "keyfile":  "mykey.key"})
    http_server.listen(8888)
    tornado.ioloop.IOLoop.instance().start()
<!DOCTYPE html>
<!-- WSSecureConnect_Client.html -->

<html lang="ja">
<head>
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta charset="utf-8" />

  <title>Secure WebSocket</title>
  <script type="text/javascript" src="WSSecureConnect_Client.js">
  </script>
  
</head>
<body>
  <div id="controll_field">
    <p>
      <input type="button" value="接続開始" onclick="startConnection()" />
      <input type="button" value="接続終了" onclick="exitConnection()" />
    </p>
    <form>
      <input type="text" name="text_field" value="ここに入力して「送信」ボタンを押してください." />
      <input type="button" name="send_button" value="送信" onclick="sendMessage(this.form.elements['text_field'].value)" />
    </form>
  </div>
  <div id="view_field">
    <p>初期テキスト</p>
  </div>
</body>
</html>
// WSSecureConnect_Client.js
'use strict';
window.addEventListener('load', function(){
    window.webSocket = null;
    window.wsServerUrl = 'wss://localhost:8888/ws_secure_connect'; // SecureWS
}, false);

window.addEventListener('unload', function(event){
    window.exitConnection();
}, false);

window.startConnection = function(event){
    window.webSocket = new WebSocket(wsServerUrl);
    window.webSocket.onopen = function(event){
       window.document.getElementById("view_field").innerHTML =
            '<p>Success to connect WebSocket Server via SecureWS!</p>'
    };

    window.webSocket.onmessage = function(event){
	alert(event.data);
	window.document.getElementById("view_field").innerHTML =
	    '<p>Server: ' + event.data + '</p>'
    };

    window.webSocket.onerror = function(error){
	document.getElementById("view_field").innerHTML = 
	    '<p>WebSocket Error: ' + error + '</p>';
    };

    window.webSocket.onclose = function(event){
	if(event.wasClean){ //切断が完全に完了したかどうかを確認
	    document.getElementById("view_field").innerHTML =
	    '<p>切断完了</p><dl><dt>Close code</dt><dd>' + 
		event.Code + 
		'</dd><dt>Close Reason</dt><dd>' + 
		event.reason +
		'</dd></dl>';
	    webSocket = null;
	}
	else
	    document.getElementById("view_field").innerHTML =
	    '<p>切断未完了</p>';
    };

}

window.exitConnection = function(event){
    if(window.webSocket != null)
	window.webSocket.close(1000, '通常終了'); //onclose関数が呼ばれる
}

window.sendMessage = function(sendingMessage){
    if(window.webSocket != null)
	window.webSocket.send(sendingMessage);
}

[Work/Class/ES2015で動的Webアプリ]

ES2015で動的Webアプリ / 2017-12-06 (水)

この授業の目的

Processingで行ったWebSocket通信を使って,ウェブブラウザ上で動くJavaScriptの2015年版(ECMAScript2015, 通称ES2015, ES6)とProcessingのJavaScript用ライブラリP5.jsを用いて,動的なWebアプリケーションを作るための基礎を学ぶ.

コンテンツ

  1. ES2015の基本 - 変数とそのスコープ,関数定義,Promiseによるコールバック
  2. ES2015の基本 - クラスの定義とインスタンス化
  3. ES2015の基本 - PromiseとXMLHttpRequestを使ったAjaxとfetch
  4. ES2015準拠でP5.jsのインスタンスモード
  5. P5.jsで日本語文字列を表示する
  6. ES2015でWebSocketの基本 - テキストメッセージの受け渡し
  7. ES2015でWebSocket - Tornadoでマルチクライアント
  8. ES2015でWebSocket - TornadoでSecureWebSocket
  9. ES2015でWebSocket - ビデオチャットのように端末側カメラの画像をWebSocketで送る

授業の下準備

実行環境としてはGoogle Chromeを使うので,最新版を入れておくこと.また描画部分にはP5.js(ProcessingのJavaScript版)を使うので,Processingの描画の復習をしておくこと.

WebSocketサーバ側は,実験用Webサーバ上で実行を行うため,MSYS2やTerminal.appを使ってUnixの基本操作に慣れておくこと.

WebSocketサーバ用のライブラリとしてはPythonのTornadoを使う.TornadoはAnacondaにパッケージがあるが,http用のローカルサーバを立てるのが面倒くさいので,基本的にはこちらで用意したサーバでサーバ側プログラムを動かす.


[Work/Class/ES2015で動的Webアプリ/WebSocketを用いた動的Webアプリ]

ES2015でWebSocket - ビデオチャットのように端末側のカメラ画像をWebSocketで送る / 2017-12-05 (火)

WebRTC

JavaScriptからWebカメラを扱う

WebRTCとは,簡単に言えば,端末のメディアデバイス(カメラやマイク)をWebブラウザのJavaScriptから扱うための規格である.Safari&iOSは11から対応している.

Videoに関しては,ChromeとSafari,つまりAndroidとiOSで書法というか扱い方の詳細に違いがあるので,クライアントのWebブラウザをを検出して,分岐させて両方書いておく必要がある.

セキュアHTTP(https://での接続)

WebRTCを使うためには,https://で始まるセキュアHTTPで接続する必要がある.

コード例

どのブラウザでアクセスしているかの文字列UserAgentを最初に取得し,iOSデバイス,Androidデバイス,PCのChrome, MacのSafariをそれぞれ切り分けて分岐処理を行う.

iOSとAndroidはスマホ本体を縦にして撮影している,つまりカメラから取得できる画像が縦長(3:4)であると想定し,PCのChromeとSafariは横長(4:3)であると想定する.

なお,ボタンをスマホブラウザから押しやすくするために,skeletonという軽量CSSフレームワークを利用している.

<!DOCTYPE html>
<!-- WSCameraImage_drawToVideoArea.html -->
<html lang="ja">
<head>
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta charset="utf-8" />
  <title>スマホやPCのカメラをWebブラウザから使う</title>
  <script type="text/javascript" src="./WSCameraImage_drawToVideoArea.js">
  </script>
  <!-- skeltonフレームワークを使う -->
  <link rel="stylesheet" href="./style/normalize.css" />
  <link rel="stylesheet" href="./style/skeleton.css" />
</head>
<body>
  <div id="display_user_agent">
  </div>
  <div>
    <!-- skeltonフレームワークを使った押しやすいボタン.クラスを指定するだけ -->
    <button class="button button=primary" onclick="startVideo()">Start</button><br/>
    <!-- ビデオ表示エリア -->
    <video id="local_video" autoplay playsinline></video>
  </div>
</body>
</html>
// WSCameraImage_drawToCanvas.js
'use strict';
function startVideo() {
    if(navigator.getUserMedia || navigator.webkitGetUserMedia || 
       navigator.mozGetUserMedia || navigator.msGetUserMedia){
	if(window.thisBrowser == 'ios'){
	    // for iOS Devices
	    // スマホを立てて撮影を想定
	    const medias = {audio: false,
			    video: {width: 480,
				    height: 640,
				    facingMode : {exact: "environment"}}};
			    // facingModeというのは,背面カメラと顔カメラのどちらを使うか
			    // 'environment'を指定すると背面カメラ,
			    // 'user'を指定すると顔カメラになる
	    navigator.getUserMedia(medias,
				   (stream) => {
				       window.localVideo.srcObject = stream;
				   },
				   (error) => {
				       alert('navigator.getUserMedia() error:' + error);
				   });
	}
	else if(window.thisBrowser == 'android'){
	    // for Android Chrome (with both Front and Back Camera)
	    // iOSと同じくスマホを立てて撮影しているのを想定
	    const medias = {audio: false,
			    video: {width: 480,
				    wheight: 640,
				    facingMode:{exact: "environment"}}};
	    navigator.mediaDevices.getUserMedia(medias) // ChromeはPCもAndroidもPromiseで記述する
		.then(stream => {
		    window.localVideo.src = window.URL.createObjectURL(stream);
		})
		.catch(error => {
		    console.error('navigator.mediaDvice.getUserMedia() error:', error);
		    return;
		});
	}
	else if(window.thisBrowser == 'pcchrome'){   
	    // PC Chrome (with only 1 Camera for Face)
	    const medias = {audio: false,
			    video: {width: 640,
				    wheight: 480}};
	    navigator.mediaDevices.getUserMedia(medias)
		.then(stream => {
		    window.localVideo.src = window.URL.createObjectURL(stream);
		})
		.catch(error => {
		    console.error('navigator.mediaDvice.getUserMedia() error:', error);
		    return;
		});
	}
	else if(window.thisBrowser == 'pcsafari'){
	    // for Mac Safari (with only 1 Camera for Face)
	    const medias = {audio: false,
			    video: {width:640, 
				    height:480}};
	    navigator.getUserMedia(medias,
				   (stream) => {
				       window.localVideo.srcObject = stream;
				   },
				   (error) => {
				       console.error('navigator.getUserMedia() error:', error);
				   });
	}
    }
}

window.addEventListener('load', () => {
    // ブラウザ情報が入っているUserAgenetを取得し,全部小文字の文字列にする
    window.userAgentInLowerCase = window.navigator.userAgent.toLowerCase();
    // UserAgentを表示
    document.getElementById("display_user_agent").innerHTML = 
	window.userAgentInLowerCase;

    // 各ブラウザで切り分ける
    if(userAgentInLowerCase.indexOf('iphone') != -1 ||
      userAgentInLowerCase.indexOf('ipad') != -1)
	window.thisBrowser = 'ios';
    if(userAgentInLowerCase.indexOf('android') != -1)
	window.thisBrowser = 'android';
    else if(userAgentInLowerCase.indexOf('chrome') != -1)
	window.thisBrowser = 'pcchrome';
    else if(userAgentInLowerCase.indexOf('safari') != -1)
	window.thisBrowser = 'pcsafari';
    else
	alert('Cannot use this program for your browser');

    window.localVideo = document.getElementById('local_video');
}, false);

window.addEventListener('unload', () => {
    
}, false);

DataURL形式

WebSocketで送るために画像を文字列にする

DataURLとは,画像や音などのメディアを,WebSocketのメッセージとして送ることができるような形式,すなわちテキスト情報にエンコードしたものである.

WebRTCから得られたビデオストリームをそのままDataURLにはできない(単に対応していないという問題だけではなく,縦横サイズを統一するために整えたりする必要がある)ので,videoタグの内容をgetImageData()関数で,クロップ(切り抜き)&縮小しながら取得し,そこで得られたImageDataオブジェクトが持つtoDatURL()関数を実行してDataURL形式に変換して取り出す.

受け取り側で復号する

DataURL形式は文字列なので,サーバ側ではこれまでのWebSocketメッセージと同じように扱うが,それを再度受け取ったクライアント側ではデコードする必要がある.この時のヘッダをsplitする文字列に注意する.

セキュアWebSocket(wss://での接続)とTornadoサーバ側の準備

WebRTCを使う場合,必ずhttps://で接続する必要があると前述したが,その結果WebSocketもセキュアWebSocket(wss://)である必要がある.


[Work/Class/サウンドトラック制作基礎]

サウンドトラック制作基礎


[Work/Class/サウンドトラック制作基礎/composite_piece]

「コード」「コードネーム」の基本

コード・コードネーム

一般的には,3つの高さの音(三和音),もしくは4つの高さの音(四和音)を同時に鳴らした場合(和音),それをコードと呼び,音の高さの組み合わせに応じて「コードネーム」が与えられる.

コードネームは,「一番低い音(基音)の高さ」と,一番目の音の「どのぐらい上」に二番目の高さの音があり,さらに二番目の高さの音の「どのぐらい上」に三番目の音が,その「どのぐらい上」に四番目の音がそれぞれ割り当てられているか,で決まる.

「どのぐらい上」を表すのに,鍵盤一つ分を半音として,その半音がいくつ間にあるか,を用いる.

この授業では,白い鍵盤のみを使う「ハ長調」(C-Majorキー)を主に用いるが,「半音」は黒い鍵盤も含めて勘定する.

基本的な三和音のコード

メジャーコード

C-Majorコードは,C(ド)を基音として,半音4つ分上に二番目の音があり,二番目の音の半音3つ分上に三番目の音をおいて構成されている.

Cメジャーコード

半音が4つ間にある間隔(音程)を「長三度」と呼び,半音が3つ間にある音程を「短三度」と呼ぶ.

Cメジャーコードから基音を上にずらしていき,二番目の音と三番目の音の間隔を変えなければ,Fメジャーコード,Gメジャーコード,を生成することができる.

基音のみ,もしくは大文字の「M」や「Maj」などをつけて,「C」「CMaj」等と表記する.

マイナーコード

Aマイナーコードは,A(ラ)を気温として,短三度上に二番目の音,その長三度上に三番目の音がある.メジャーコードの音程をひっくり返すとマイナーコードになる.

Aマイナーコード

基音をずらしていくと同様に,Dマイナーコード,Eマイナーコードを生成することができる.

マイナーコードを表記するには,基音の後に小文字の「m」をつけ,「Am」等と表記する.

その調(キー)と同じ基音を持つコード(ハ長調C-Majorであれば,Cコード)を「主和音」と呼ぶ.

基本的な四和音のコード

同様に,長三度と短三度の組み合わせを3回(つまり4音)重ねたのが,セブンスコード,メジャーセブンスコード,マイナーセブンスコード,マイナーセブンスフラットファイブである.

特にセブンスコードとマイナーセブンスフラットファイブは一つの長調(Majorキー)の中にそれぞれ一つしか存在せず,強烈にそのキーの主和音へ移行したくなる感覚を与える性質がある.

セブンスコード

クラシックでは属七とも呼ばれる.ハ長調(C-Major)ではGセブンスがそれにあたる.

G7コード

音程の構成は下から,長三度,短三度,短三度である.

基本的にはこのセブンスコードと,三和音のメジャーマイナーだけで曲を更生することができる.

「基音7」,つまり「G7」と表記する.

メジャーセブンスコード

一つの長調の中に2つ存在する.ハ長調(C-Major)の場合は,CメジャーセブンスとFメジャーセブンスである.

Cメジャー7thコード

音程の構成は下から,長三度,短三度,長三度である.

「基音M7」「基音Maj7」等と表記する.「CMaj7」

マイナーセブンスコード

一つの長調の中に,3つ存在する.ハ長調(C-Major)の場合は,Dマイナーセブンス,Eマイナーセブンス,Aマイナーセブンスである.

Aマイナー7thコード

音程の構成は下から,短三度,長三度,短三度である.

「基音m7」等と表記する.「Am7」

マイナーセブンスフラットファイブ

マイナーセブンスの第三音(五度)が半音下がった(♭した)形なのでこう呼ぶ.一つの長調の中に1つのみ存在する.ハ長調(C-Major)の場合はBマイナーセブンスフラットファイブである.セブンスコードと同じく,次の音として強く主和音を求める.

Bマイナーセブンスフラットファイブコード

音程の構成は下から,短三度,短三度,長三度である.

「基音m7♭5」「基音m7-5」「基音m7 5-」などと表記する.「-5」「5-」は右上に小さく表記されることが多い.