SQLの窓

2018年11月15日


WebRTC による WEBカメラ表示( 2018/11/15 ) : iPhone + Safari は iOS11 のみで確認

過去何度も繰り返して来た『WebRTC による WEBカメラ表示』ですが、いろいろ変遷ありつつこのような形で現在動作確認しています。Chrome と Edge で確認したところによると、旧 API でも動作しています。

デモページ

▼ MDN 資料
MediaDevices - Web API インターフェイス | MDN
Taking still photos with WebRTC - Web APIs | MDN

今回、iPhone の iOS11 で 動作確認したところ、旧 API 側で動作したようです。しかし、iOS12 では Safari 側でカメラとマイクを利用すると設定した上で、実行時に許可する必要があるのですが( iOS11 未確認 : PCでは基本的に皆そうです )、うまく動作せず検証は保留となっています( デバイス所持者が自分以外なので )

※ 共通事項 : インターネット上では SSL である事
※ WebRTC が使えない場合は動画で代替え
<!DOCTYPE html>
<html>
<head>
<meta content="width=device-width initial-scale=1.0 minimum-scale=1.0 maximum-scale=1.0 user-scalable=no" name="viewport">
<meta charset="UTF-8">
<link rel="shortcut icon" href="https://winofsql.jp/WinOfSql.ico">

<title>WEBカメラの表示</title>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.1/css/bootstrap.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.4/toastr.min.js"></script>

<style>
@media screen and ( min-width:480px ) {
	#content {
		padding: 20px;
	}
	#camera {
		width: 480px;
		height: 360px;
	}
}
@media screen and ( max-width:479px ) {
	#content {
		padding: 0px;
	}
	#camera {
		width: 100%;
	}
}

</style>
</head>
<body>
<div id="content">

	<video id="camera" autoplay></video>
	<a class="btn btn-secondary btn-sm float-right ml-4"
		href=".">フォルダ</a>

	<a class="btn btn-secondary btn-sm float-right ml-4"
		onclick="$('#camera').css({'width':'100%', 'height':'100%'})"
		href="#">100%</a>

	<a class="btn btn-secondary btn-sm float-right"
		onclick="$('#camera').css({'width':'auto', 'height':'auto'})"
		href="#">auto</a>

</div>

<script>
// *************************************
// 簡易スマホチェック
// *************************************
jQuery.isMobile = (/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(navigator.userAgent.toLowerCase()));
toastr.options={"closeButton":false,"debug":false,"newestOnTop":false,"progressBar":false,"positionClass":"toast-bottom-center","preventDuplicates":false,"onclick":null,"showDuration":"300","hideDuration":"1000","timeOut":"3000","extendedTimeOut":"1000","showEasing":"swing","hideEasing":"linear","showMethod":"fadeIn","hideMethod":"fadeOut"};
if ( !$.isMobile ) {
	toastr.options.positionClass = "toast-top-center";
}

// *************************************
// localhost 以外では SSL で処理する
// *************************************
if ( location.host != "localhost" ) {
	if ( location.protocol == "http:" ) {
		location.protocol = "https:"
	}
}

// *************************************
// カメラ参照
// *************************************
// カメラ用 video 要素(DOM オブジェクト)
var camera;
camera = $("#camera").get(0);

// *************************************
// 最新 API
// *************************************
if ( navigator.mediaDevices ) {

	// カメラ表示
	navigator.mediaDevices.getUserMedia({video: true})
	.then(function(stream){
		// カメラのストリームを表示
		camera.srcObject = stream;
	})
	.catch(function(err){
		// ブラウザで使用を拒否した場合等( 動画で代替 )
		errorVideo();
	});
}
// *************************************
// 旧 API
// *************************************
else {
	// 旧 WebRTCチェック用
	var api = [
		"webkitGetUserMedia", "mozGetUserMedia","msGetUserMedia"
	]
	$.each(api,function(idx){
		if (navigator.getUserMedia = navigator.getUserMedia || navigator[api[idx]]) {
			return false;
		}
	});
	// WebRTC 使用可能
	if ( navigator.getUserMedia ) {
		// スマホでは、幅いっぱいに使う
		if ( $.isMobile ) {
			$("#camera").css("width","100%");
		}
		// カメラの表示
		navigator.getUserMedia({video: true}, 
			function(stream) {
				// カメラのストリームを表示
				camera.srcObject = stream;
				// ▼ 旧実装
				// camera.src = window.URL.createObjectURL(stream);
			},
			function(err){
				// ブラウザで使用を拒否した場合等( 動画で代替 )
				errorVideo();
			}
		);	
	}
	else {
		// 動画で代替
		errorVideo();
	}
}

// *************************************
// 動画で代替
// *************************************
function errorVideo() {

	toastr.error( "WebRTC を使用できません");

	$("#camera")
	.prop({ 
		"loop" : true, "muted" : true, "controls" : true,
		"src" : "TriggerRally.mp4"
	})
	.css({"border": "solid 1px #000"});

}
</script>

</body>
</html>

参考

Apple Safari の WebRTC について
WebRTC で URL.createObjectURLはまもなく使えなくなる




posted by lightbox at 2018-11-15 15:53 | API | このブログの読者になる | 更新情報をチェックする

2017年05月02日


PHP で Mastodon にアプリを登録して投稿する手順

MastodonOAuthPHP をダウンロードして使用します。通信部分で、file_get_contents が使われているので、他方面でも参考になるでしょう。

必要なファイルは以下の三つ
1) HttpRequest.php (通信)
2) oAuth.php
3) Mastodon.php (API)
 ※ Mastodon API overview

ただ、ちょっと古い php のバージョンを使っている場合はエラーとなるので、Mastodon.php の 81 行目を変更する必要があります。( 5.4 以前では言語構造である empty の 引数に関数の戻り値を指定できないので、いったん変数にセットして使う)

※ ソースコードは全て UTF8N で保存

PHP 5.4 以前での変更箇所
    /**
     * Post a new status to your {visibility} timeline
     * @param type $text
     * @param type $visibility
     */
    public function postStatus($text = "", $visibility = "public", $in_reply_to_id = null){
        $credentials = $this->getCredentials();
        if(!empty($credentials)){
            
            $headers = $this->getHeaders();
            
            //Create our object
            $http = HttpRequest::Instance($this->getApiURL());
            $status = $http::Post(
                "api/v1/statuses",
                array(
                    "status"        => $text,
                    "visibility"    => $visibility,
                    "in_reply_to_id" => $in_reply_to_id
                ),
                $headers
            );
            return $status;
        }
        return false;
    }


PHP でリダイレクトする URL の設定

oAuth.php の中で3箇所あります。最初の値は、redirect_uris なので、複数の URL を指定できるようですが、ここでは一つの localhost の テスト用の redirect.php を指定しています。
    private $app_config = array(
        "client_name"   => "MastoTweet",
        "redirect_uris" => "http://localhost/0502/redirect.php",
        "scopes"        => "read write",
        "website"       => "https://www.thecodingcompany.se"
    );

    public function getAuthUrl(){
        if(is_array($this->credentials) && isset($this->credentials["client_id"])){
            
            //Return the Authorization URL
            return "https://{$this->mastodon_api_url}/oauth/authorize/?".http_build_query(array(
                    "response_type"    => "code",
                    "redirect_uri"     => "http://localhost/0502/redirect.php",
                    "scope"            => "read write",
                    "client_id"        => $this->credentials["client_id"]
                ));
        }        
        return false;        
    }

    public function getAccessToken($auth_code = ""){
        
        if(is_array($this->credentials) && isset($this->credentials["client_id"])){
            
            //Request access token in exchange for our Authorization token
            $http = HttpRequest::Instance("https://{$this->mastodon_api_url}");
            $token_info = $http::Post(
                "oauth/token",
                array(
                    "grant_type"    => "authorization_code",
                    "redirect_uri"  => "http://localhost/0502/redirect.php",
                    "client_id"     => $this->credentials["client_id"],
                    "client_secret" => $this->credentials["client_secret"],
                    "code"          => $auth_code
                ),
                $this->headers
            );
            
            //Save our token info
            return $this->_handle_bearer($token_info);
        }
        return false;
    }



最初の処理 : アプリの登録

登録と言っても、実行する毎に client_id と client_secret が取得されます。実行は一度きりでいいですが、後でアクセストークンを取得するので、いったんファイルに保存します

require_once は、autoload.php のみでできるように example.php に書かれていますが、逆に初見では解りづらいのでカレントに置いて全て require_once しています。

mstdn.jp は、登録したいインスタンスのドメインです。

create_app.php
<?php

require_once("HttpRequest.php");
require_once("oAuth.php");
require_once("Mastodon.php");

$mastodon = new \theCodingCompany\Mastodon("mstdn.jp");

$token_info = $mastodon->createApp("appname", "サイトURL");
// appname : 認証済みアプリに表示される id
// サイトURL : 上記のリンク先となる URL

print "<pre>";
print_r( $token_info );
print "</pre>";

file_put_contents("token_info.txt", json_encode( $token_info, JSON_PRETTY_PRINT ) );

?>


アクセストークンの取得

いわゆる認証のいつもの流れですが、アクセストークンを取得する為の code を取得する URL を取得して、その URL に header 関数でリダイレクトします。

get_token.php
<?php

require_once("HttpRequest.php");
require_once("oAuth.php");
require_once("Mastodon.php");

$mastodon = new \theCodingCompany\Mastodon("mstdn.jp");

// client_id と client_secret のセット
$mastodon->setCredentials( json_decode( file_get_contents('token_info.txt'), true ) );

// アクセストークン取得の為のトークンを取得する URL
$auth_url = $mastodon->getAuthUrl();

// その URL をブラウザで表示
header("Location: $auth_url");

?>


そうすると、ログインしていなければ以下の画面になります



そして、ログインすると承認画面です。



承認したら、oAuth.php で指定した場所に飛ばされるので、以下のソースコードで受け取ります。

redirect.php
<?php
header( "Content-Type: text/html; Charset=utf-8" );

require_once("HttpRequest.php");
require_once("oAuth.php");
require_once("Mastodon.php");

$mastodon = new \theCodingCompany\Mastodon("mstdn.jp");
// client_id と client_secret のセット
$mastodon->setCredentials( json_decode( file_get_contents('token_info.txt'), true ) );

$access_token = $mastodon->getAccessToken($_GET['code']);
file_put_contents("token_info.txt", json_encode( $mastodon->getCredentials(), JSON_PRETTY_PRINT ) );
?>

AccessToken を取得して保存しました


これで準備は完了です。あとは FORM 作って投稿です。

mstdn_post.php
<?php
if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {

	require_once("HttpRequest.php");
	require_once("oAuth.php");
	require_once("Mastodon.php");

	$mastodon = new \theCodingCompany\Mastodon("mstdn.jp");
	// client_id と client_secret と AccessToken のセット
	$mastodon->setCredentials( json_decode( file_get_contents('token_info.txt'), true ) );

	$mastodon->postStatus($_POST[text]);

}

?>
<!DOCTYPE html>
<html>
<head>
<meta content="width=device-width initial-scale=1.0 minimum-scale=1.0 maximum-scale=1.0 user-scalable=no" name="viewport">
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
</head>
<body>
<form method="post">
	<div>
		<textarea name="text" style='width:400px;height:200px;'></textarea>
	</div>
	<input name="send" type="submit" value="send">
</form>

</body></html>




タグ:MASTODON PHP API
posted by lightbox at 2017-05-02 17:44 | API | このブログの読者になる | 更新情報をチェックする

2017年04月23日


Google スプレッドシートの内容を JSON として localhost で取得する手順

Google API

Google API Console で、プロジェクトを作成し、Google Apps API より Sheets API をクリックして有効にする。

認証情報を作成で、API キーを選択。

最初はテストの為、キーの制限は『なし』のままで作業する。

Google ドライブ

Google スプレッドシートを作成する。



『無題のスプレッドシート』をクリックして名前を付けて、適当な表データを作成する。



共有の設定で、『リンクを知っている全員が閲覧できる』ようにする。



URL 部分より、スプレッドシートの id を取得する



JSON 取得用の URL

https://sheets.googleapis.com/v4/spreadsheets/{spreadsheetId}/values/{range} をベースに Google Chrome のアドレスバーで、JSON 取得用の URL を完成させる。

range 部分には シート名を日本語のまま貼り付ける。

最後に、?key=APIキーで完成させる
https://sheets.googleapis.com/v4/spreadsheets/1TEAEfUkxObUasSPdqml0qKb-TR2sA34a6m5ONCfb8eU/values/%E3%82%B7%E3%83%BC%E3%83%881?key=APIキー
{
  "range": "'シート1'!A1:Z1000",
  "majorDimension": "ROWS",
  "values": [
    [
      "228291",
      "310689",
      "307080"
    ],
    [
      "21631",
      "11969",
      "19389"
    ],
    [
      "209800",
      "183450",
      "177000"
    ],
    [
      "35375",
      "35471",
      "38699"
    ],
    [
      "6278",
      "7587",
      "8671"
    ],
    [
      "8452",
      "8930",
      "10415"
    ]
  ]
}

PHP で実行

まず、localhost で実行してみる。

▼ UTF-8N で保存
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Google スプレッドシート</title> 
</head>
<body style='white-space:pre;'>
<?php

$url = "https://sheets.googleapis.com/v4/spreadsheets/1TEAEfUkxObUasSPdqml0qKb-TR2sA34a6m5ONCfb8eU/values/%E3%82%B7%E3%83%BC%E3%83%881?key=APIキー";
$data = @file_get_contents($url);

print $data;

?>
</body>
</html>


API キーの制限を 『IP アドレス』にして、『127.0.0.1』と設定する。その後、ブラウザからアクセスすると、403 エラーとなって、アクセスして来た IP アドレスが表示されるので、その IP アドレスを登録します。すると、その場所の WAN 側のアドレスからのアクセス時のみ受け付けるようになります。




タグ:API google
posted by lightbox at 2017-04-23 20:58 | API | このブログの読者になる | 更新情報をチェックする

2017年02月06日


WebRTC による WEBカメラ表示を canvas にコピーして画像に変換し、サーバへアップロードする

資料ページ

Taking still photos with WebRTC
2016/3末時点のWebRTCブラウザ対応状況まとめ

※ デモページは、WEBアプリ用のテンプレートを Bootstrap を使用してスマホ対応で作成しています

▼ デモページ

※ この画像では、カメラが無い場合の代替の動画を使用しています

MediaDevices - Web API インターフェイス | MDN
Taking still photos with WebRTC - Web APIs | MDN



▼ 初回( Google Chrome )


ソースコード

カメラを使うにしても、動画を使うにしても VIDEO 要素が使用されます。そこから、いったん canvas へコピーして、base64 で表現された画像に変換します。

アップロード時は、FormData を使い、データはバイナリに変換してアップロードします。(最大3枚までアップロード可能にしています)

アップロードは ajax で行われるので、ページが書き換わる事はありません。結果として返される json は、$_FILES の内容をそのまま返しています(メッセージを追加しています)

<!DOCTYPE html>
<html lang="ja">
<head>
<meta content="width=device-width initial-scale=1.0 minimum-scale=1.0 maximum-scale=1.0 user-scalable=no" name="viewport">
<meta charset="utf-8">
<title>カメラ撮影とアップロード</title>

<!-- jQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<!-- jQuery UI -->
<link id="link" rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.10.1/themes/base/jquery-ui.css">
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha/css/bootstrap.css">
<!-- jQuery.mmenu -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jQuery.mmenu/5.5.3/core/js/jquery.mmenu.min.all.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jQuery.mmenu/5.5.3/core/css/jquery.mmenu.all.css">
<!-- toastr -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.3/toastr.min.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.3/toastr.min.js"></script>

<link rel="stylesheet" href="std/mmenu.css">
<link rel="stylesheet" href="std/basic.css">

<style>
.fields {
	width: 85px;
	font-size: 12px;
	vertical-align: middle!important;
}

legend {
	font-size: 18px;
	padding-left: 6px;
}

/* 画像表示用 */
#row2 {
	vertical-align: top!important;
}

/* カメラ用 */
#camera {
	width: 400px;
	height: 300px;
	object-fit: fill;
}
#canvas {
	/* display: none; */
}
</style>

<script>
jQuery.isMobile = (/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(navigator.userAgent.toLowerCase()));
toastr.options={"closeButton":false,"debug":false,"newestOnTop":false,"progressBar":false,"positionClass":"toast-bottom-center","preventDuplicates":false,"onclick":null,"showDuration":"300","hideDuration":"1000","timeOut":"3000","extendedTimeOut":"1000","showEasing":"swing","hideEasing":"linear","showMethod":"fadeIn","hideMethod":"fadeOut"};
if ( !$.isMobile ) {
	toastr.options.positionClass = "toast-top-center";
}
var datepicker_option = {
	dateFormat: 'yy/mm/dd',
	dayNamesMin: ['日', '月', '火', '水', '木', '金', '土'],
	monthNames:  ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
	showMonthAfterYear: true,
	yearSuffix: '年',
	changeYear: true,
	showAnim: 'fadeIn',
	yearRange: "c-70:c"
}
$(function(){
	var curlink = $("#title").text();
	$("#title").html("<a href=\"./\" style=\"color:#fff\">" + curlink + "</a>");
});
var options = {
	row1 : { title : "カメラ" },
	row2 : { title : ""  },
	row3 : { title : "画像一覧<br>(ロード順)"  },
	row4 : { title : ""  },
	row_last : { title :"メッセージ" },
	error : function(message){
		$("#row_last").next().text( message );
		toastr.error(message);
	},
	info : function(message){
		$("#row_last").next().text( message );
		toastr.success(message);
	},
	cerror : function( message ){
		message =  message + "<br>代替として動画を表示します"
		$("#row_last").next().html( message );
		toastr.error( message );
		$("#camera")
		.prop({ 
			"loop" : true, "muted" : true, "controls" : true,
			"src" : "mp4/freebies_018_win.mp4"
		})
		.css("border", "solid 1px #000");
	}
};

// *************************************
// カメラ用データ
// *************************************
var camera;
var canvas;
var copy_count = 0;

$(function(){

	// 1) options による行とフィールドの設定
	// 2) Bootstrap 用 form-control クラスの追加
	$(".fields").each(function(){
		if ( options[ $(this).prop("id") ] ) {
			$(this).html( options[ $(this).prop("id") ].title );
			// 個別 css
			if ( options[ $(this).prop("id") ].css ) {
				$(this).next().find("input,select").css( options[ $(this).prop("id") ].css );
			}
			// 入力チェック用属性
			if ( options[ $(this).prop("id") ].attr ) {
				$(this).next().find("input,select").attr( options[ $(this).prop("id") ].attr );
			}
		}
		$(this).next().find("input,select").addClass("form-control");
	});
	// スマホでロード時の処理のチラつき防止用
	$("#wrapper").css({"visibility":"visible", "margin-bottom" : "0px" }); 

	// 初期フォーカス
	setTimeout( function(){$('#row1_fld').focus();}, 100 );

	// video 内 camera
	camera = $("#camera").get(0);

	// *************************************
	// 最新 API
	// *************************************
	if ( navigator.mediaDevices ) {
		console.log("navigator.mediaDevices");

		if ( $.isMobile ) {
			$("#camera").css("width","100%");
		}
		navigator.mediaDevices.getUserMedia({video: true})
		.then(function(stream){
			camera.srcObject = stream;
		})
		.catch(function(err){
			// ブラウザで使用を拒否した場合等( 動画で代替 )
			options.cerror(err.name);
		});

	}
	// *************************************
	// 旧 API
	// *************************************
	else {
		console.log("navigator.getUserMedia");

		// 旧 WebRTCチェック用
		var api = [
			"webkitGetUserMedia", "mozGetUserMedia","msGetUserMedia"
		]
		$.each(api,function(idx){
			if (navigator.getUserMedia = navigator.getUserMedia || navigator[api[idx]]) {
				return false;
			}
		});
		// WebRTC 使用可能
		if ( navigator.getUserMedia ) {
			if ( $.isMobile ) {
				$("#camera").css("width","100%");
			}
			// カメラの表示
			navigator.getUserMedia({video: true}, 
				function(stream) {
					camera.src = window.URL.createObjectURL(stream);
				},
				function(err){
					// ブラウザで使用を拒否した場合等( 動画で代替 )
					options.cerror(err.name);
				}
			);	
		}
		else {
			// WebRTC 使用不可( 動画で代替 )
			options.cerror("WebRTC を使用できません");
		}
	}

	// *************************************
	// canvas にコピーして画像に変換
	// *************************************
	$("#copy").on( "click", function(){

		copy_count++;
		if ( copy_count > 3 ) {
			options.error("撮影は3枚までです");
			return false;
		}

		canvas = $("#canvas").get(0);
		var ctx = canvas.getContext('2d');

		ctx.drawImage(camera, 0, 0, canvas.width, canvas.height);

		$("<img>").appendTo("#images")
		.prop( {"src": canvas.toDataURL("image/jpeg"), "id": "image"+ copy_count } )
		.css( {"width": "100px", "margin": "10px" } );


	});

	// *************************************
	// アップロード処理
	// *************************************
	$("#frm").submit( function(event){
		// 本来の送信処理はキャンセル
		event.preventDefault();

		if ( $("#images").html() == "" ) {
			options.error("アップロードする画像ファイルを作成して下さい");
			return;
		}

		$("fieldset").eq(0).prop("disabled", true);

		// エラーメッセージエリアをクリア
		$(".error").next().text( "" );

		// 結果の表示エリアを全てクリア
		$("#result").html( "" );


		// **************************************
		// ファイルのアップロード
		// **************************************
		console.log("アップロード処理開始");

		var formData = new FormData();

		// テストの為、約100K の制限
		formData.append("MAX_FILE_SIZE", 100000);

		var file_cnt = 0;

		$("#images img").each( function() {

			var base64 = $(this).prop("src");
			var bin = atob(base64.split(',')[1]);
			var buffer = new Uint8Array(bin.length);
			for (var i = 0; i < bin.length; i++) {
				buffer[i] = bin.charCodeAt(i);
			}
			var blob = new Blob([buffer.buffer], {type: "image/jpeg"});

			file_cnt++;
			var file_name = (new Date()).getTime();
			formData.append("image"+file_cnt, blob, file_name +"_"+file_cnt+".jpg");

		});

		formData.append("FILE_COUNT", file_cnt );

		$.ajax({
			url: "./upload.php",
			type: "POST",
			data: formData,
			processData: false,  // jQuery がデータを処理しないよう指定
			contentType: false   // jQuery が contentType を設定しないよう指定
		})
		.done(function( data, textStatus ){
			console.log( "status:" + textStatus );
			console.log( "data:" + JSON.stringify(data, null, "    ") );
			options.info("アップロード処理が完了しました");

			// アップロード結果の表示
			$.each(data, function(idx,image){

				if ( image.error != 0 ) {
					$("#result").append("<tr><td><span id=\"result" +idx+"\"></span><b style='color:red'>" + image.name+ " : " + image.result +"</b></td></tr>");
				}
				else {
					$("#result").append("<tr><td><span id=\"result" +idx+"\"></span>" + image.name + " : " + image.result +"</td></tr>");
				}

				$( "#result"+idx ).append($("#"+idx).clone());

			});

			$("#images").html("");
			copy_count = 0;
		})
		.fail(function(jqXHR, textStatus, errorThrown ){
			console.log( "status:" + textStatus );
			console.log( "errorThrown:" + errorThrown );
			options.info("アップロードに失敗しました");
		})
		.always(function() {

			// 操作不可を解除
			$("fieldset").eq(0).prop("disabled", false);
		})
		;

	} );

	// **************************************
	// mmenu
	// **************************************
	$("#mmenu_left").mmenu({
		navbar: {
			title: "メニュー"
		},
		offCanvas: {
			position  : "left",
			zposition : "next"
		}
	});


});

</script>
</head>
<body>

<div id="wrapper">
<script>
// スマホでロード時等の処理のチラつき防止用
$("#wrapper").css( {"visibility": "hidden", "margin-bottom" : "1000px" } );
</script>

	<div id="head">
		<a id="hamburger" href="#mmenu_left">
	<span class="top-bar"></span>
	<span class="middle-bar"></span>
	<span class="bottom-bar"></span>
</a>
		<div id="title">カメラ撮影とアップロード</div>
	</div>

	<div id="body">
		<form id="frm" class="form-inline">

			<fieldset>
				<legend>アップロード</legend>
				<table class="table table-condensed">
			
					<tr>
						<td class="fields" id="row1"></td>
						<td>
							<video
								id="camera"
								autoplay></video>
							<canvas
								id="canvas"
								width="400"
								height="300"></canvas>								
						</td>
					</tr>

					<tr>
						<td class="fields" id="row2"></td>
						<td>
							<input id="copy" type="button" class="btn btn-primary btn-sm" value="撮影">
						</td>
					</tr>

					<tr>
						<td class="fields" id="row3"></td>
						<td>
							<div id="images"></div>
						</td>
					</tr>

					<tr>
						<td class="fields" id="row4"></td>
						<td>
							<input id="action" type="submit" class="btn btn-primary btn-sm" value="送信">
						</td>
					</tr>

					<tr>
						<td class="fields error" id="row_last"></td>
						<td></td>
					</tr>

				</table>

			</fieldset>

			<fieldset>
				<legend>結果</legend>
				<table id="result" class="table table-condensed">


				</table>

			</fieldset>

		</form>
	</div>

	<div id="comment">
	ようこそ jQuery + Bootstrap(css) + mmenu + WebRTC(カメラ) + FormData + PHP<br><a href="https://www.studio-lab01.com/freebies.html" target="_blank">素材提供:らぼわん</a> / カメラが無い場合の動画素材	</div>

</div>


<div id="mmenu_left">
<ul>
	<li class="mm_user_title">ページ選択</li>
	<li><a class="mm_link_left" href="#" onclick="location='index.php';void(0)">リセット</a></li>
	<li><a class="mm_link_left" 
			href="http://getbootstrap.com/css/"
			onclick="location='index.php';void(0)"
			target="_blank"
		>Bootstrap(css)</a></li>
	<li><a class="mm_link_left"
			href="http://api.jquery.com/"
			onclick="location='index.php';void(0)"
			target="_blank"
		>jQuery ドキュメント</a></li>

	<li><a class="mm_link_left"
			href="https://developer.mozilla.org/ja/docs/Web/Guide/Using_FormData_Objects"
			onclick="location='index.php';void(0)"
			target="_blank"
		>FormData オブジェクトの利用 / MDN</a></li>

	<li><a class="mm_link_left"
			href="https://developer.mozilla.org/ja/docs/Web/API/MediaDevices"
			onclick="location='index.php';void(0)"
			target="_blank"
		>MediaDevices (MDN)</a></li>

	<li><a class="mm_link_left"
			href="https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Taking_still_photos"
			onclick="location='index.php';void(0)"
			target="_blank"
		>Taking still photos with WebRTC(英文)</a></li>


</ul>
</div>


</body>
</html>



upload.php

※ アップロードした画像の保存部分はコメントにしています
<?php
// キャラクタセット
// *************************************
header( "Content-Type: application/json; charset=utf-8" );
// *************************************
// キャッシュ無効
// *************************************
session_cache_limiter('nocache');
session_start();

// ファイルを移動するフォルダ
$target_folder = "./images/";


$cnt = $_POST["FILE_COUNT"] + 0;

for( $i = 0; $i < $cnt; $i++ ) {

	$image_target = "image".($i+1);

	if ( $_FILES[$image_target]["error"] == 0 ) {

		// *************************************
		// 1) 画像フォーマットの取得
		// *************************************
		$type_string = image_type_to_mime_type( exif_imagetype( $_FILES[$image_target]['tmp_name'] ) );

		// *************************************
		// 2) オリジナルファイル名の取得
		// *************************************
		$file = explode(".", $_FILES[$image_target]['name']);

		// *************************************
		// 3) 日本語ファイル名対応
		// *************************************
		$file_name = urlencode( $file[0] );

		// *************************************
		// 4) 保存ファイル名を作成
		//   a) 拡張子決定
		//   b) uniqid() でファイル目をユニーク
		// *************************************
		$target = "";
		if ( $type_string == "image/jpeg" ) {
			$target = uniqid() . "_{$file_name}.jpg";
		}
		if ( $type_string == "image/gif" ) {
			$target = uniqid() . "_{$file_name}.gif";
		}
		if ( $type_string == "image/png" ) {
			$target = uniqid() . "_{$file_name}.png";
		}
		if ( $target == "" ) {
			$_FILES["image"]["result"][] = "アップロードできないフォーマットです";
		}
		else {
			// *************************************
			// アップロードファイルの保存
			// *************************************
//			if ( @move_uploaded_file( $_FILES[$image_target]['tmp_name'], $target_folder . $target ) ) {
				$_FILES[$image_target]["result"] = "アップロードに成功しました";
//			}
//			else {
				// なんらかの環境エラー
//				$_FILES[$image_target]["result"] = "アップロードに失敗しました";
//			}
			
		}
	}
	else {
		switch($_FILES[$image_target]["error"]){
			case 1:
				$_FILES[$image_target]["result"] = "php.ini の upload_max_filesize ディレクティブの値を超えています";
				break;
			case 2:
				$_FILES[$image_target]["result"] = "HTML フォームで指定された MAX_FILE_SIZE を超えています";
				break;
			case 3:
				$_FILES[$image_target]["result"] = "一部のみしかアップロードされていません";
				break;
			case 4:
				$_FILES[$image_target]["result"] = "アップロードされませんでした";
				break;
			case 6:
				$_FILES[$image_target]["result"] = "テンポラリフォルダがありません";
				break;
			case 7:
				$_FILES[$image_target]["result"] = "ディスクへの書き込みに失敗しました";
				break;
			case 8:
				$_FILES[$image_target]["result"] = "PHP の拡張モジュールがファイルのアップロードを中止しました";
				break;
			default:
				$_FILES[$image_target]["result"] = "不明なエラーです";
		}
		
	}

}


print json_encode($_FILES)


?>


関連する記事

WebRTC による WEBカメラ表示



posted by lightbox at 2017-02-06 18:18 | API | このブログの読者になる | 更新情報をチェックする

2017年01月24日


WebRTC による WEBカメラ表示

資料ページ

Taking still photos with WebRTC
2016/3末時点のWebRTCブラウザ対応状況まとめ

※ デモページは、WEBアプリ用のテンプレートを Bootstrap を使用してスマホ対応で作成しています

▼ デモページ


MediaDevices - Web API インターフェイス | MDN
Taking still photos with WebRTC - Web APIs | MDN



▼ 初回( Google Chrome )


▼ カメラが無い場合


ソースコード

当初、古い API を実装していたのですが、いろいろ調べるうちに新しい API に変わっていた事に気がついて、両方のコードが実装されています。
WebRTC の処理部分は 119行 〜 183行です。

※ 処理画面部分は 220行 〜 256行
※ 画面構築しているテンプレート部分は、64行 〜 86行 と 98行 〜 116行

<!DOCTYPE html>
<html lang="ja">
<head>
<meta content="width=device-width initial-scale=1.0 minimum-scale=1.0 maximum-scale=1.0 user-scalable=no" name="viewport">
<meta charset="utf-8">
<title>カメラ表示</title>

<script src="//ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<link id="link" rel="stylesheet" href="//ajax.googleapis.com/ajax/libs/jqueryui/1.10.1/themes/base/jquery-ui.css">
<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha/css/bootstrap.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jQuery.mmenu/5.5.3/core/js/jquery.mmenu.min.all.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jQuery.mmenu/5.5.3/core/css/jquery.mmenu.all.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.3/toastr.min.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/2.1.3/toastr.min.js"></script>

<link rel="stylesheet" href="../std/mmenu.css">
<link rel="stylesheet" href="../std/basic.css">

<style>
.fields {
	width: 85px;
	font-size: 12px;
	vertical-align: middle!important;
}

legend {
	font-size: 18px;
	padding-left: 6px;
}

/* カメラ用 */
#camera {
	width: 400px;
	height: 300px;
	object-fit: fill;
}

.table-responsive td, .table-responsive th {
	white-space: nowrap;
}
</style>

<script>
jQuery.isMobile = (/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(navigator.userAgent.toLowerCase()));
toastr.options={"closeButton":false,"debug":false,"newestOnTop":false,"progressBar":false,"positionClass":"toast-bottom-center","preventDuplicates":false,"onclick":null,"showDuration":"300","hideDuration":"1000","timeOut":"3000","extendedTimeOut":"1000","showEasing":"swing","hideEasing":"linear","showMethod":"fadeIn","hideMethod":"fadeOut"};
if ( !$.isMobile ) {
	toastr.options.positionClass = "toast-top-center";
}
var datepicker_option = {
	dateFormat: 'yy/mm/dd',
	dayNamesMin: ['日', '月', '火', '水', '木', '金', '土'],
	monthNames:  ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
	showMonthAfterYear: true,
	yearSuffix: '年',
	changeYear: true,
	showAnim: 'fadeIn',
	yearRange: "c-70:c"
}
$(function(){
	var curlink = $("#title").text();
	$("#title").html("<a href=\"./\" style=\"color:#fff\">" + curlink + "</a>");
});
var options = {
	row1 : { title : "カメラ" },
	row_last : { title :"メッセージ" },
	error : function(message){
		$("#row_last").next().text( message );
		toastr.error(message);
	},
	info : function(message){
		$("#row_last").next().text( message );
		toastr.success(message);
	},
	cerror : function( message ){
		message =  message + "<br>代替として動画を表示します"
		$("#row_last").next().html( message );
		toastr.error( message );
		$("#camera")
		.prop({ 
			"loop" : true, "muted" : true, "controls" : true,
			"src" : "https://lightbox.sakura.ne.jp/demo/mp4/waterfall-free-video9.mp4"
		})
		.css("border", "solid 1px #000");
	}
};

// *************************************
// カメラ用データ
// *************************************
var camera;
var canvas;

$(function(){

	// 1) options による行とフィールドの設定
	// 2) Bootstrap 用 form-control クラスの追加
	$(".fields").each(function(){
		if ( options[ $(this).prop("id") ] ) {
			$(this).html( options[ $(this).prop("id") ].title );
			// 個別 css
			if ( options[ $(this).prop("id") ].css ) {
				$(this).next().find("input,select").css( options[ $(this).prop("id") ].css );
			}
			// 入力チェック用属性
			if ( options[ $(this).prop("id") ].attr ) {
				$(this).next().find("input,select").attr( options[ $(this).prop("id") ].attr );
			}
		}
		$(this).next().find("input,select").addClass("form-control");
	});
	// スマホでロード時の処理のチラつき防止用
	$("#wrapper").css({"visibility":"visible", "margin-bottom" : "0px" }); 

	// 初期フォーカス(row1_fld があれば機能します)
	setTimeout( function(){$('#row1_fld').focus();}, 100 );

	// video 内 camera
	camera = $("#camera").get(0);

	// *************************************
	// 最新 API
	// *************************************
	if ( navigator.mediaDevices ) {
		if ( $.isMobile ) {
			$("#camera").css("width","100%");
		}
		navigator.mediaDevices.getUserMedia({video: true})
		.then(function(stream){
			camera.src = window.URL.createObjectURL(stream);
		})
		.catch(function(err){
			// ブラウザで使用を拒否した場合等( 動画で代替 )
			options.cerror(err.name);
		});

		// デバイス一覧
		$("#result").append("<tr><th>label</th><th>deviceId</th><th>kind</th></tr>");
		navigator.mediaDevices.enumerateDevices()
		.then(function(devices) {
			devices.forEach(function(device) {
				$("#result").append("<tr><td>" + device.label + "</td><td>" + device.deviceId +"</td><td>"+ device.kind + "</td></tr>");
			});
		})
		.catch(function(err) {
			options.error(err.name + ": " + err.message);
		});

	}
	// *************************************
	// 旧 API
	// *************************************
	else {
		// 旧 WebRTCチェック用
		var api = [
			"webkitGetUserMedia", "mozGetUserMedia","msGetUserMedia"
		]
		$.each(api,function(idx){
			if (navigator.getUserMedia = navigator.getUserMedia || navigator[api[idx]]) {
				return false;
			}
		});
		// WebRTC 使用可能
		if ( navigator.getUserMedia ) {
			if ( $.isMobile ) {
				$("#camera").css("width","100%");
			}
			// カメラの表示
			navigator.getUserMedia({video: true}, 
				function(stream) {
					camera.src = window.URL.createObjectURL(stream);
				},
				function(err){
					// ブラウザで使用を拒否した場合等( 動画で代替 )
					options.cerror(err.name);
				}
			);	
		}
		else {
			// WebRTC 使用不可( 動画で代替 )
			options.cerror("WebRTC を使用できません");
		}
	}

	// **************************************
	// mmenu
	// **************************************
	$("#mmenu_left").mmenu({
		navbar: {
			title: "メニュー"
		},
		offCanvas: {
			position  : "left",
			zposition : "next"
		}
	});


});

</script>
</head>
<body>

<div id="wrapper">
<script>
// スマホでロード時の処理のチラつき防止用
$("#wrapper").css( {"visibility": "hidden", "margin-bottom" : "1000px" } );
</script>

	<div id="head">
		<a id="hamburger" href="#mmenu_left">
	<span class="top-bar"></span>
	<span class="middle-bar"></span>
	<span class="bottom-bar"></span>
</a>
		<div id="title">カメラ表示</div>
	</div>

	<div id="body">
		<form id="frm" class="form-inline">

			<fieldset>
				<legend></legend>
				<table class="table table-condensed">
			
					<tr>
						<td class="fields" id="row1"></td>
						<td>
							<video
								id="camera"
								autoplay></video>
						</td>
					</tr>

					<tr>
						<td class="fields error" id="row_last"></td>
						<td></td>
					</tr>

				</table>

			</fieldset>

			<fieldset>
				<legend>結果</legend>
				<div class="table-responsive">
					<table id="result" class="table table-condensed">

					</table>
				</div>

			</fieldset>

		</form>
	</div>

	<div id="comment">
	ようこそ jQuery + Bootstrap(css) + mmenu + WebRTC(カメラ)	</div>

</div>


<div id="mmenu_left">
<ul>
	<li class="mm_user_title">ページ選択</li>
	<li><a class="mm_link_left" href="#" onclick="location='index.php';void(0)">リセット</a></li>
	<li><a class="mm_link_left" 
			href="http://getbootstrap.com/css/"
			onclick="location='index.php';void(0)"
			target="_blank"
		>Bootstrap(css)</a></li>
	<li><a class="mm_link_left"
			href="http://api.jquery.com/"
			onclick="location='index.php';void(0)"
			target="_blank"
		>jQuery ドキュメント</a></li>

	<li><a class="mm_link_left"
			href="https://developer.mozilla.org/ja/docs/Web/API/MediaDevices"
			onclick="location='index.php';void(0)"
			target="_blank"
		>MediaDevices (MDN)</a></li>


</ul>
</div>


</body>
</html>


関連する記事

WebRTC による WEBカメラ表示を canvas にコピーして画像に変換し、サーバへアップロードする



posted by lightbox at 2017-01-24 15:09 | API | このブログの読者になる | 更新情報をチェックする

2016年12月16日


Amazon API の 503エラー の対処について

長く使ってなかった Amazon の 商品情報を取得するコードを実行していたら、商品単位で一回目のアクセスに、かなりの確率で 503 エラーで失敗するという現象に出くわしました。ページをリロードすると、たいていは表示されますが、リロードできない仕様のページもあるので調べてみると、『Amazonマーケットプレイス Web サービス (Amazon MWS) ドキュメント』にこんな事が書いてありました

500エラー、または503エラーを受信後、オペレーションの呼び出しをリトライする場合は、最初のエラーレスポンス直後にリトライすることができます。複数回リトライする場合、Amazonでは最大リトライ回数4回の「Exponential backoff」(指数関数的後退)によるアプロ―ディを推奨します。その後エラーを記録し、手動のフォローアップと調査を進めます。の例えば、リトライの時間を、1秒、4秒、10秒、30秒の間隔で計ることができます。実際の後退時間と制限は、あなたのビジネスプロセスによって異なります。
という事で、手動のリロードは1秒程度なので、1秒づつ待って4回リトライして正常動作を確認しました。
if ( !$dom->load($req2) ) {
	sleep(1);
	if ( !$dom->load($req2) ) {
		sleep(1);
		if ( !$dom->load($req2) ) {
			sleep(1);
			if ( !$dom->load($req2) ) {
				sleep(1);
				if ( !$dom->load($req2) ) {
					print "<b style='color:white;font-size:24px;'>リロードして下さい <input type='button' value='リロード' onclick='location.reload(true)'> </b>";
				}
			}
		}
	}

}




posted by lightbox at 2016-12-16 09:40 | API | このブログの読者になる | 更新情報をチェックする
Seesaa の各ページの表示について
Seesaa の 記事がたまに全く表示されない場合があります。その場合は、設定> 詳細設定> ブログ設定 で 最新の情報に更新の『実行ボタン』で記事やアーカイブが最新にビルドされます。

Seesaa のページで、アーカイブとタグページは要注意です。タグページはコンテンツが全く無い状態になりますし、アーカイブページも歯抜けページはコンテンツが存在しないのにページが表示されてしまいます。

また、カテゴリページもそういう意味では完全ではありません。『カテゴリID-番号』というフォーマットで表示されるページですが、実際存在するより大きな番号でも表示されてしまいます。

※ インデックスページのみ、実際の記事数を超えたページを指定しても最後のページが表示されるようです

対処としては、このようなヘルプ的な情報を固定でページの最後に表示するようにするといいでしょう。具体的には、メインの記事コンテンツの下に『自由形式』を追加し、アーカイブとカテゴリページでのみ表示するように設定し、コンテンツを用意するといいと思います。


※ エキスパートモードで表示しています

アーカイブとカテゴリページはこのように簡単に設定できますが、タグページは HTML 設定を直接変更して、以下の『タグページでのみ表示される内容』の記述方法で設定する必要があります

<% if:page_name eq 'archive' -%>
アーカイブページでのみ表示される内容
<% /if %>

<% if:page_name eq 'category' -%>
カテゴリページでのみ表示される内容
<% /if %>

<% if:page_name eq 'tag' -%>
タグページでのみ表示される内容
<% /if %>
この記述は、以下の場所で使用します
container 終わり



フリーフォントで簡単ロゴ作成
フリーフォントでボタン素材作成
フリーフォントで吹き出し画像作成
フリーフォントではんこ画像作成
ほぼ自由に利用できるフリーフォント
フリーフォントの書体見本とサンプル
画像を大きく見る為のウインドウを開くボタンの作成

CSS ドロップシャドウの参考デモ
イラストAC
ぱくたそ
写真素材 足成
フリーフォント一覧
utf8 文字ツール
右サイド 終わり
base 終わり