How to establish an SSL connection to an MS AD LDAP server with a self-signed certificate in Java language.

Before starting, ensure that the CA root certificate is available.
Create a key store using the following command.

keytool -import -trustcacerts -file CA_root_certificate_file -keystore keystore_file_name

For testing purposes, let’s assume the keystone_file_name is “jssecacerts".

When the above command is executed, the prompt “Enter keystore password:" will appear.
For testing purposes, we set the password to “123456″.
Then the prompt “Re-enter new password:" will appear. Simply enter “123456″ and then press “enter". After that, the certificate details will be shown on the screen,
and the prompt “Trust this certificate? [no]:" will appear. Type “y" to complete the creation of the keystore.

To establish an SSL connection with the LDAP server, you need to create an SSL socket factory class., the sample code as below:

import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyStore;
import java.util.concurrent.atomic.AtomicReference;

import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;

public class SSLSocketFactory extends SocketFactory{
	private static final AtomicReference<MySSLSocketFactory> defaultFactory = new AtomicReference<>();
	private SSLSocketFactory sf;
	public MySSLSocketFactory() {
      try {
    	KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
    	/*
    	 * Use the keytool command to import the hko CA root cert. to the keystore file ./jssecacerts 
    	 */
    	FileInputStream fis = new FileInputStream("./jssecacerts");
    	keyStore.load(fis, "123456".toCharArray());
    	
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(keyStore);
        SSLContext ctx = SSLContext.getInstance("TLS");
        ctx.init(null, tmf.getTrustManagers(), null);
        sf = ctx.getSocketFactory();
      } catch (Exception ff) {
    	  ff.printStackTrace();
      }
      
    }
	public static SocketFactory getDefault() {
        final MySSLSocketFactory value = defaultFactory.get();
        if (value == null) {
            defaultFactory.compareAndSet(null, new MySSLSocketFactory());
            return defaultFactory.get();
        }
        return value;
    }
	@Override
	public Socket createSocket(String arg0, int arg1) throws IOException, UnknownHostException {
		// TODO Auto-generated method stub
		return sf.createSocket(arg0, arg1);
	}

	@Override
	public Socket createSocket(InetAddress arg0, int arg1) throws IOException {
		// TODO Auto-generated method stub
		return sf.createSocket(arg0, arg1);
	}

	@Override
	public Socket createSocket(String arg0, int arg1, InetAddress arg2, int arg3)
			throws IOException, UnknownHostException {
		// TODO Auto-generated method stub
		return sf.createSocket(arg0, arg1,arg2,arg3);
	}

	@Override
	public Socket createSocket(InetAddress arg0, int arg1, InetAddress arg2, int arg3) throws IOException {
		// TODO Auto-generated method stub
		return sf.createSocket(arg0, arg1,arg2,arg3);
	}

}

A sample LDAP query is as below:

Hashtable<String, String> environment = new Hashtable<String, String>();		
environment.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
environment.put(Context.SECURITY_AUTHENTICATION, "simple");
environment.put(Context.SECURITY_PRINCIPAL, user_name);
environment.put(Context.SECURITY_CREDENTIALS, password);
/*  
To connect to the LDAP server via SSL channel add the following environmental variables
*/
environment.put(Context.SECURITY_PROTOCOL, "ssl");
environment.put("java.naming.ldap.factory.socket", "SSLSocketFactory");
environment.put(Context.PROVIDER_URL, "ldaps://server_url");
DirContext context = null;
try {
	context = new InitialDirContext(environment);
	String filter = "(CN=John)";
	String[] attrIDs = { "*" };
	String fieldName;
	SearchControls searchControls = new SearchControls();
	searchControls.setReturningAttributes(attrIDs);
	searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
	NamingEnumeration<SearchResult> searchResults = context.search("DC=ad,DC=com", filter,
			searchControls);

	while (searchResults.hasMoreElements()) {
		SearchResult result = searchResults.nextElement();
		NamingEnumeration<? extends Attribute> attrs = result.getAttributes().getAll();

		while (attrs.hasMore()) {
			Attribute a = attrs.nextElement();
			fieldName = a.getID();
			System.out.print("attribute name=" + fieldName);
			System.out.println(",value=" + result.getAttributes().get(fieldName).get());
		}
		System.out.println("=====================================================");
	}
	context.close();
} catch (javax.naming.AuthenticationException er) {
	System.out.println("Invalid user name or password.");
} catch (NamingException e) {
	// TODO Auto-generated catch block
	e.printStackTrace();
}
Reference web page:
https://stackoverflow.com/questions/23144353/how-do-i-initialize-a-trustmanagerfactory-with-multiple-sources-of-trust
https://stackoverflow.com/questions/4615163/how-to-accept-self-signed-certificates-for-jndi-ldap-connections

#java

#Microsoft Active Directory

#LDAP

如何用 WebRTC API來制作網頁即時通訊應用程式(傳送媒體篇)

經過上一篇之後,雙方已經連線,要進行即時通訊,少不免要交換媒體,這篇文章會介紹如何將本地媒體(包括video 和audio)經WebRTC API 傳到遠方。

取得本地媒體:

程式碼如下:

const constraint = {
		    "audio":true,
		    "video":true
		   };
try{
     let stream = await navigator.mediaDevices.getUserMedia(constraint);
}catch (error){
 console.log(error);
 return null;
}

這裡constraint 是指定想取得什麼媒體,以上例子只是想取得audio和video而已,沒有指定audio和video 的質素,例如想指定audio 的sampleSize為16bit,雙聲道, video 的高度為480px, video 的闊度640px,那麼constraint 會變成以下這樣:

constraint = {
               "audio":{
                 channelCount: 2,  
                 sampleSize: 16 
               },
	       "video": {
                  width: 640,
                  height: 480,
                }
};

如果想知道constraint 內有什麼attribute 可用,可以按這裡

將本地媒體(包括video 和audio)經WebRTC API傳到遠方:

程式碼如下:

for (const track of stream.getTracks()) {
    peerConnection.addTrack(track, stream);
}

如何停止將本地媒體(包括video 和audio)經WebRTC API傳到遠方:

  1. 停止本地媒體流:

    stream.getTracks().forEach(async track => {
    await track.stop();
    });


  2. 從peerConnection 移除媒體流:

    peerConnection.getSenders().forEach(sender => {
    peerConnection.removeTrack(sender);
    });

就是這麼簡單,現附上簡單sample code 給大家參考:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<style>
			.normalRow{
				height:10%
			}
			.statusCaption{
				align-self:center;
				display:flex;
				flex-grow:1;
				padding:0px;
				margin:0px;
			}
			.statusCell{
				display:flex;
				flex-direction:row;
				gap:0px;
				height:100%;
				margin:0px;
				padding:0px;
				position:relative;
			}
			.statusText{
				display:flex;
				flex-grow:9;
				padding:0px;
				margin:0px
			}
			.videoCell{
				position:relative;
			}
			.videoRow{
				height:40%
			}
			html{ 
			  height:100%;
			  margin:0px;
			  padding:0px;
			}
			body{ 
			  height:calc(100% - 20px);
			  margin:10px;
			  padding:0px;
			}
			table{
				height:100%;
				width:100%;
				margin:0px;
				padding:0px;
			}	
			
			textarea{
				left:0px;
				margin:0px;
				top:0px;
				vertical-align: middle;
				width:100%;
			}
			video{
				height:calc(100% - 6px);
				left:0px;
				margin:3px;
				object-fit:cover;
				position:absolute;
				top:0px;
				width:calc(100% - 6px);
			}
		</style>
		<script>
			function LocalMedia(){
				const constraint = {
					"audio":true,
					"video":true
				};
				this.close = async (stream) => {
					if (stream) {
						stream.getTracks().forEach(async track => {
							await track.stop();
						});
					}
				}
				this.get=async()=>{
					try{
						return await navigator.mediaDevices.getUserMedia(constraint);
					}catch (error){
						console.log(error);
						return null;
					}
				}
			}
			function WebRTCConnection(id) {
				let config = {
					iceServers: [
						{
							urls: [
								"stun:stun.l.google.com:19302",
								"stun:stun1.l.google.com:19302",
								"stun:stun2.l.google.com:19302",
								"stun:stun3.l.google.com:19302",
								"stun:stun4.l.google.com:19302",
							]
						},						
						{
							urls: 'turn:openrelay.metered.ca:80?transport=tcp',
							username: 'openrelayproject',
							credential: 'openrelayproject'
						}
					]
				}
				let dataChannel = null,peerConnection=null,stream=null;
				let ignoreOffer = false,makingOffer = false, polite = false;
				let iceCandidateHandler, negotiationHandler,trackHandler;
				let localMedia=new LocalMedia();
				this.call = () => {
					polite = true;					
					initDataChannel(peerConnection.createDataChannel("chat"));
				}
				this.addICECandidate = async(iceCandidate) => {
					if (peerConnection.connectionState !=="closed"){
						await peerConnection.addIceCandidate(iceCandidate);
					}
				}
				this.hangUp=async()=>{
					if (stream) {
						await localMedia.close(stream);
					}
					if (peerConnection && (peerConnection.signalingState !== "closed")) {
						peerConnection.getSenders().forEach(sender => {
							peerConnection.removeTrack(sender);
						});
						peerConnection.close();
					}
				}
				this.init=()=>{
					peerConnection=new RTCPeerConnection(config);
					peerConnection.ondatachannel = (event) => {
						initDataChannel(event.channel);
					}
					peerConnection.onconnectionstatechange = ()=>{
						connectionStateChangeHandler();
					}
					peerConnection.onicecandidate = e=>{
						if (e.candidate) {
							msgLogger(id+" received an ice candidate.");
							iceCandidateHandler(e.candidate);
						}
					}
					peerConnection.onnegotiationneeded =  async () => {
						msgLogger("====Negotiation start====");
						try {
							makingOffer = true;
							await peerConnection.setLocalDescription();
							msgLogger(id+" local description is generated.");
							negotiationHandler(peerConnection.localDescription);
						} catch (err) {
							msgLogger("Failed to send Local Description:" + err);
						} finally {
							makingOffer = false;
							msgLogger("====Negotiation end====");
						}
					}
					peerConnection.ontrack = event => {
						msgLogger(id+" received track event");
						trackHandler(event.streams[0]);
					};
					msgLogger(id+" init completed");
				}				
				this.on=(eventType,handler)=>{
					switch (eventType){
						case "iceCandidate":
							iceCandidateHandler =handler;
							break;
						case "negotiation":
							negotiationHandler = handler;
							break;
						case "track":
							trackHandler = handler;
							break;
						default:
							break
					}
				}
				this.setRemoteDescription = async (remoteDescription) => {
					msgLogger("====processRemoteDescription Start====");
					const offerCollision = (remoteDescription.type === "offer") &&
						(makingOffer || peerConnection.signalingState !== "stable");
					ignoreOffer = !polite && offerCollision;
					msgLogger("remoteDescription.type=" + remoteDescription.type + ",makingOffer=" + makingOffer + ", peerConnection.signalingState=" + peerConnection.signalingState);
					msgLogger("ignoreOffer = " + ignoreOffer + ",offerCollision=" + offerCollision + ",polite=" + polite);
					if (ignoreOffer) {
						msgLogger("Ignore offer from " + this.peerName);
						return;
					}
					try{
						await peerConnection.setRemoteDescription(remoteDescription);
					}catch (error){
						msgLogger("peerConnection.signalingState="+peerConnection.signalingState);
						msgLogger("An error occur when setting remote description.");
						msgLogger(error);
					}
					if (remoteDescription.type === "offer") {
						try{
							await peerConnection.setLocalDescription();
							negotiationHandler(peerConnection.localDescription);
							msgLogger(id+" local description is generated.");
						}catch (error){
							msgLogger("peerConnection.signalingState="+peerConnection.signalingState);
							msgLogger("An error occur when setting local description.");
							msgLogger(error);
						}                
					}
					msgLogger("====processRemoteDescription End====");
				}
				this.shareMedia=async()=>{
					stream=await localMedia.get();					
					if (stream !== null){
						for (const track of stream.getTracks()) {
							peerConnection.addTrack(track, stream);
						}
						document.getElementById("callerMedia").srcObject=stream;
					}
				}
				this.stopShareMedia=async()=>{
					await localMedia.close(stream);
					peerConnection.getSenders().forEach(sender => {
						peerConnection.removeTrack(sender);
					});
				}
				//========================================================
				//  Private function
				//========================================================
				let connectionStateChangeHandler=()=>{
					msgLogger(id+" Connection state="+peerConnection.connectionState);
				}
				
				/*=====================================================================*/
				/*        Initialize the data channel and its event handler            */
				/*=====================================================================*/
				let initDataChannel = (channel) => {
					dataChannel = channel;
					dataChannel.onclose = () => {
						msgLogger("DataChannel is closed!");
					};
					dataChannel.onerror = (event) => {
						msgLogger("An error occured in DataChannel:"+JSON.stringify(event));
					};
					dataChannel.onmessage = (message) => {
						msgLogger("Received Message from DataChannel");
					};
					dataChannel.onopen = () => {
						msgLogger("DataChannel is opened!");
					};
				}
				
				let msgLogger = (msg) => {
					let textArea=document.getElementById(id+"Status");
					textArea.value+=msg+"\n";
				}
			};
			let callee,caller;
			function call(e){
				e.preventDefault();				
				caller.call();
			}
			function hangUp(e){
				e.preventDefault();
				caller.hangUp();
			}
			function init(){
				caller=new WebRTCConnection("caller");
				callee=new WebRTCConnection("callee");
				callee.init();
				caller.init();
				caller.on("negotiation",localDescription=>{
					callee.setRemoteDescription(localDescription);
				});
				callee.on("negotiation",localDescription=>{
					caller.setRemoteDescription(localDescription);
				});
				caller.on("iceCandidate",iceCandidate=>{
					callee.addICECandidate(iceCandidate);
				});
				callee.on("iceCandidate",iceCandidate=>{
					caller.addICECandidate(iceCandidate);
				});
				callee.on("track", stream=>{
					document.getElementById("calleeMedia").srcObject=stream;
				});
			}
			async function shareMedia(e){
				let value=e.target.checked;				
				if (e.target.checked){
					await caller.shareMedia();
				}else {
					await caller.stopShareMedia();
				}
			}
		</script>
	</head>
	<body onload="init()">
		<table border="1">
			<tr class="normalRow">
				<td>&nbsp;Caller</td>
				<td>&nbsp;Callee</td>
			</tr>
			<tr class="videoRow">
				<td class="videoCell"><video autoplay controls muted id="callerMedia"></video></td>
				<td class="videoCell"><video autoplay controls muted id="calleeMedia"></video></td>
			</tr>
			<tr class="normalRow">
				<td>
					<button onclick="call(event)">Call</button>
					<button onclick="hangUp(event)">Hang up</button>
					<input type="checkbox" onclick="shareMedia(event)" value="1">Share Media</button>
				</td>
				<td></td>
			</tr>
			<tr class="videoRow">
				<td style="padding:0px;margin:0px;">
					<div class="statusCell">
						<div class="statusCaption">status:</div>
						<div class="statusText"><textarea id="callerStatus"></textarea></div>
					</div>
				</td>
				<td style="padding:0px;margin:0px;">
					<div class="statusCell">
						<div class="statusCaption">status:</div>
						<div class="statusText"><textarea id="calleeStatus"></textarea></div>
					</div>
				</td>
			</tr>
		</table>
	</body>
</html>

PS:出於保安考慮,所有媒體流一定要用SSL來加密,如果想在web server上運行以上HTML, 這個web server 一定要安裝SSL certificate才可以成功取得本地媒體。

#javascript
#WebRTC

如何用 WebRTC API來制作網頁即時通訊應用程式(建立連線實作篇)

根據上一篇的流程,先要起始RTCPeerConnection object,程式碼如下:

let config = {
		iceServers: [
			{
				urls: [
					"stun:stun.l.google.com:19302",
					"stun:stun1.l.google.com:19302",
					"stun:stun2.l.google.com:19302",
					"stun:stun3.l.google.com:19302",
					"stun:stun4.l.google.com:19302",
				]
			},						
			{
				urls: 'turn:openrelay.metered.ca:80?transport=tcp',
				username: 'openrelayproject',
				credential: 'openrelayproject'
			}
		]
	}
    peerConnection=new RTCPeerConnection(config);
	peerConnection.ondatachannel = (event) => {
		initDataChannel(event.channel);
	}
	peerConnection.onicecandidate = e=>{
		if (e.candidate) {
			iceCandidateHandler(e.candidate);
		}
	}
	peerConnection.onnegotiationneeded =  async () => {
		msgLogger("====Negotiation start====");
		try {
			makingOffer = true;
			await peerConnection.setLocalDescription();
			msgLogger(id+" local description is generated.");
			negotiationHandler(peerConnection.localDescription);
		} catch (err) {
			msgLogger("Failed to send Local Description:" + err);
		} finally {
			makingOffer = false;
			msgLogger("====Negotiation end====");
		}
	}

在起始RTCPeerConnection object 時, 最少要設定一些ice server為建立連線時做準備。

peerConnection=new RTCPeerConnection(config);

至於ICE server 可以在那裡找到,我也是在google 打"free turn server list" 來找的,
當然有些server 可用,有些不可用,大家可以自行安裝sturn 和turn server,
也可以用付費的server。
除此ice server之外, 這個config. 還有其他起始設定可以set, 詳細可以參考網址:

https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection

另外,還要設定兩個重要event handler, 它們是ICE candidate 和 negotiation event handler。根據上一篇的流程,當以下method 被call 時,

RTCPeerConnection.createDataChannel 和
RTCPeerConnection.addTrack

就會觸發negotiation event,negotiation event handler的用途就是generate local description,然後將local Description經signal server傳送給對方。

在呼叫 setLocalDescription 之後會觸發一次或多次icecandidate event,每觸發一次就收到一個ice candidate,直至收到的ice candidate是null 為止,然後將收到ice candidate經signal server 傳送給對方。

當建立連線成功後,datachannel event會被觸發,那時我們也要針對不同的data channel event
(例如 channel open, channel close)去做處理。

什麼是signal server?
signal server 就是充當中間人角色,幫連線雙方交換local description
和ICE candidate,對網頁應用程式來說可以是web socket server 和application server,這方面
W3C 沒有規定用什麼方法做,反而留給應用程式師根據自身情況自行決定。

Perfect Negotiation
用了這個logic, 無論caller 和callee 雙方都可以用樣的程式碼來建立連線,還可以避免在generate offer途中接受連線而出現問題,而使連線失敗。

this.setRemoteDescription = async (remoteDescription) => {
		msgLogger("====processRemoteDescription Start====");
		const offerCollision = (remoteDescription.type === "offer") &&
			(makingOffer || peerConnection.signalingState !== "stable");
		ignoreOffer = !polite && offerCollision;
		msgLogger("remoteDescription.type=" + remoteDescription.type + ",makingOffer=" + makingOffer + ", peerConnection.signalingState=" + peerConnection.signalingState);
		msgLogger("ignoreOffer = " + ignoreOffer + ",offerCollision=" + offerCollision + ",polite=" + polite);
		if (ignoreOffer) {
			msgLogger("Ignore offer from " + this.peerName);
			return;
		}
		try{
			await peerConnection.setRemoteDescription(remoteDescription);
		}catch (error){
			msgLogger("peerConnection.signalingState="+peerConnection.signalingState);
			msgLogger("An error occur when setting remote description.");
			msgLogger(error);
		}
		if (remoteDescription.type === "offer") {
			try{
				await peerConnection.setLocalDescription();
				negotiationHandler(peerConnection.localDescription);
				msgLogger(id+" local description is generated.");
			}catch (error){
				msgLogger("peerConnection.signalingState="+peerConnection.signalingState);
				msgLogger("An error occur when setting local description.");
				msgLogger(error);
			}                
		}
		msgLogger("====processRemoteDescription End====");
	}

我發覺用了Perface Negotiation之後,連線速度快了,成功率高了很多。關於Perface Negotiation可以參考以下網址:
https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation

現附上一個簡單sample 給大家參考:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<style>
			.normalRow{
				height:10%
			}
			.statusCaption{
				align-self:center;
				display:flex;
				flex-grow:1;
				padding:0px;
				margin:0px;
			}
			.statusCell{
				display:flex;
				flex-direction:row;
				gap:0px;
				height:100%;
				margin:0px;
				padding:0px;
				position:relative;
			}
			.statusText{
				display:flex;
				flex-grow:9;
				padding:0px;
				margin:0px
			}
			.videoCell{
				position:relative;
			}
			.videoRow{
				height:40%
			}
			html{ 
			  height:100%;
			  margin:0px;
			  padding:0px;
			}
			body{ 
			  height:calc(100% - 20px);
			  margin:10px;
			  padding:0px;
			}
			table{
				height:100%;
				width:100%;
				margin:0px;
				padding:0px;
			}	
			
			textarea{
				left:0px;
				margin:0px;
				top:0px;
				vertical-align: middle;
				width:100%;
			}
			video{
				height:calc(100% - 6px);
				left:0px;
				margin:3px;
				object-fit:cover;
				position:absolute;
				top:0px;
				width:calc(100% - 6px);
			}
		</style>
		<script>
			function WebRTCConnection(id) {
				let config = {
					iceServers: [
						{
							urls: [
								"stun:stun.l.google.com:19302",
								"stun:stun1.l.google.com:19302",
								"stun:stun2.l.google.com:19302",
								"stun:stun3.l.google.com:19302",
								"stun:stun4.l.google.com:19302",
							]
						},						
						{
							urls: 'turn:openrelay.metered.ca:80?transport=tcp',
							username: 'openrelayproject',
							credential: 'openrelayproject'
						}
					]
				}
				let dataChannel = null,peerConnection=null,stream=null;
				let ignoreOffer = false,makingOffer = false, polite = false;
				let iceCandidateHandler, negotiationHandler,trackHandler;
				this.call = () => {
					polite = true;
					initDataChannel(peerConnection.createDataChannel("chat"));					
				}
				this.addICECandidate = async(iceCandidate) => {
					switch (peerConnection.connectionState){
						case "connected":
						case "connecting":
						case "new":
							await peerConnection.addIceCandidate(iceCandidate);
					}
				}
				this.hangUp=()=>{
					peerConnection.close();
				}
				this.init=()=>{
					peerConnection=new RTCPeerConnection(config);
					peerConnection.ondatachannel = (event) => {
						initDataChannel(event.channel);
					}
					peerConnection.onconnectionstatechange = ()=>{
						connectionStateChangeHandler();
					}
					peerConnection.onicecandidate = e=>{
						if (e.candidate) {
							msgLogger(id+" received an ice candidate.");
							iceCandidateHandler(e.candidate);
						}
					}
					peerConnection.onnegotiationneeded =  async () => {
						msgLogger("====Negotiation start====");
						try {
							makingOffer = true;
							await peerConnection.setLocalDescription();
							msgLogger(id+" local description is generated.");
							negotiationHandler(peerConnection.localDescription);
						} catch (err) {
							msgLogger("Failed to send Local Description:" + err);
						} finally {
							makingOffer = false;
							msgLogger("====Negotiation end====");
						}
					}
					peerConnection.ontrack = event => {
						msgLogger(id+" received track event");
						trackHandler(event.streams[0]);
					};
					msgLogger(id+" init completed");
				}
				this.on=(eventType,handler)=>{
					switch (eventType){
						case "iceCandidate":
							iceCandidateHandler =handler;
							break;
						case "negotiation":
							negotiationHandler = handler;
							break;
						case "track":
							trackHandler = handler;
							break;
						default:
							break
					}
				}
				this.setRemoteDescription = async (remoteDescription) => {
					msgLogger("====processRemoteDescription Start====");
					const offerCollision = (remoteDescription.type === "offer") &&
						(makingOffer || peerConnection.signalingState !== "stable");
					ignoreOffer = !polite && offerCollision;
					msgLogger("remoteDescription.type=" + remoteDescription.type + ",makingOffer=" + makingOffer + ", peerConnection.signalingState=" + peerConnection.signalingState);
					msgLogger("ignoreOffer = " + ignoreOffer + ",offerCollision=" + offerCollision + ",polite=" + polite);
					if (ignoreOffer) {
						msgLogger("Ignore offer from " + this.peerName);
						return;
					}
					try{
						await peerConnection.setRemoteDescription(remoteDescription);
					}catch (error){
						msgLogger("peerConnection.signalingState="+peerConnection.signalingState);
						msgLogger("An error occur when setting remote description.");
						msgLogger(error);
					}
					if (remoteDescription.type === "offer") {
						try{
							await peerConnection.setLocalDescription();
							negotiationHandler(peerConnection.localDescription);
							msgLogger(id+" local description is generated.");
						}catch (error){
							msgLogger("peerConnection.signalingState="+peerConnection.signalingState);
							msgLogger("An error occur when setting local description.");
							msgLogger(error);
						}                
					}
					msgLogger("====processRemoteDescription End====");
				}				
				//========================================================
				//  Private function
				//========================================================
				let connectionStateChangeHandler=()=>{
					msgLogger(id+" Connection state="+peerConnection.connectionState);
				}
				
				/*=====================================================================*/
				/*        Initialize the data channel and its event handler            */
				/*=====================================================================*/
				let initDataChannel = (channel) => {
					dataChannel = channel;
					dataChannel.onclose = () => {
						msgLogger("DataChannel is closed!");
					};
					dataChannel.onerror = (event) => {
						msgLogger("An error occured in DataChannel:"+event);
					};
					dataChannel.onmessage = (message) => {
						msgLogger("Received Message from DataChannel");
					};
					dataChannel.onopen = () => {
						msgLogger("DataChannel is opened!");
					};
				}
				
				let msgLogger = (msg) => {
					let textArea=document.getElementById(id+"Status");
					textArea.value+=msg+"\n";
				}
			};
			let callee,caller;
			function call(e){
				e.preventDefault();
				callee.init();
				caller.init();
				caller.on("negotiation",localDescription=>{
					callee.setRemoteDescription(localDescription);
				});
				callee.on("negotiation",localDescription=>{
					caller.setRemoteDescription(localDescription);
				});
				caller.on("iceCandidate",iceCandidate=>{
					callee.addICECandidate(iceCandidate);
				});
				callee.on("iceCandidate",iceCandidate=>{
					caller.addICECandidate(iceCandidate);
				});
				caller.call();
			}
			function hangUp(e){
				e.preventDefault();
				caller.hangUp();
			}
			function init(){
				caller=new WebRTCConnection("caller");
				callee=new WebRTCConnection("callee");				
			}
		</script>
	</head>
	<body onload="init()">
		<table border="1">
			<tr class="normalRow">
				<td>&nbsp;Caller</td>
				<td>&nbsp;Callee</td>
			</tr>
			<tr class="videoRow">
				<td class="videoCell"><video autoplay controls muted id="callerMedia"></video></td>
				<td class="videoCell"><video autoplay controls muted id="calleeMedia"></video></td>
			</tr>
			<tr class="normalRow">
				<td>
					<button onclick="call(event)">Call</button>
					<button onclick="hangUp(event)">Hang up</button>
				</td>
				<td></td>
			</tr>
			<tr class="videoRow">
				<td style="padding:0px;margin:0px;">
					<div class="statusCell">
						<div class="statusCaption">status:</div>
						<div class="statusText"><textarea id="callerStatus"></textarea></div>
					</div>
				</td>
				<td style="padding:0px;margin:0px;">
					<div class="statusCell">
						<div class="statusCaption">status:</div>
						<div class="statusText"><textarea id="calleeStatus"></textarea></div>
					</div>
				</td>
			</tr>
		</table>
	</body>
</html>	


#javascript
#WebRTC

如何用 WebRTC API來制作網頁即時通訊應用程式(建立連線流程)

建立連線流程:

Step呼叫者(Caller)接聽者(Callee)
1起始RTCPeerConnection object起始RTCPeerConnection object
2call RTCPeerConnection.createDataChannel來generate 新的data channel.
3在call RTCPeerConnection.createDataChannel之後會觸發negotiation event
4在negotiation event handler 中呼叫 setLocalDescription function 來generate Local Description
5將Local Description(又名offer)經signal server 傳送給callee將caller 的offer 加入RTCPeerConnection object
6在呼叫 setLocalDescription 之後會觸發一次或多次icecandidate event, 每觸發一次就收到一個ice candidate,直至收到的ice candidate是null 為止,然後將收到ice candidate經signal server 傳送給callee。呼叫 setLocalDescription function 來generate Local Description
7將Local Description(又名answer)經signal server 傳送給caller
8將callee 的answer 加入RTCPeerConnection object將收到的caller ice candidate 加入RTCPeerConnection object
9在呼叫 setLocalDescription 之後會觸發一次或多次icecandidate event, 每觸發一次就收到一個ice candidate,直至收到的ice candidate是null 為止,然後將收到ice candidate經signal server 傳送給caller。
10將收到的caller ice candidate 加入RTCPeerConnection object
如果一切順利,雙方的RTCPeerConnection.connectionState 會變成 connected。

PS: 除了call RTCPeerConnection.createDataChannel 之外, call RTCPeerConnection.addTrack 也可以觸發negotiation event。

Mozilla Web Docs
https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API
https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Signaling_and_video_calling

W3C 的 WebRTC Recommendation
https://www.w3.org/TR/webrtc/

#javascript
#Web RTC

Inline Event Handler with an event object

Although the inline event handler for HTML elements is not recommended, it is still supported by Chrome and Firefox, I just want to write an example for record only. Consider the following code:

     <script>
        function go(){
            console.log("Hi");
        }
     </script>
     <form>
	<button onclick="go()">Say Hi</button>
     </form>

When a user clicks the button, by default, the form will be submitted. To prevent the form from submission, we can use the e.preventDefault() to do so. But, how to pass the event object to the event handler, The sample code is as below:

    <script>
        function go(e){
            e.preventDefault();
            console.log("Hi");
        }
     </script>
     <form>
	<button onclick="go(event)">Say Hi</button>
     </form>

To pass an event object to the event handler, you must pass the “event" variable to the event handler, which means the following code is not work.

<button onclick="go(abc)">Init</button>

You have to use the following code to make it work properly.

<button onclick="go(event)">Say Hi</button>

After the code is changed, when a user clicks the button, the form will be submitted and the word “Hi" will be shown in the console.

#javascript

#html

如何用 WebRTC API來制作網頁即時通訊應用程式 (WebRTC的基礎知識)

由於2020年的疫症令我對用web browser 來進行video conferencing產生興趣,
一經google 之下,發現原來已經有人制定相關的API標準, 這個標準的名稱就叫做WebRTC (全名Web Real-Time Communication), 而這個標準的目的是給web browser 和mobile device 之間進行Real-Time Communication之用。

以下browser 已經support WebRTC:

  • Desktop PC:
    • Microsoft Edge 12+[25]
    • Google Chrome 28+
    • Mozilla Firefox 22+[26]
    • Safari 11+[27]
    • Opera 18+[28]
    • Vivaldi 1.9+
  • Android
    • Google Chrome 28+ (enabled by default since 29)
    • Mozilla Firefox 24+[29]
    • Opera Mobile 12+
  • Chrome OS
  • Firefox OS
  • BlackBerry 10
  • iOS MobileSafari/WebKit (iOS 11+)
  • Tizen 3.0

以下是一些基本terminology大家要認識的:

  • Local /Remote Description: 就是指進行video conferencing 的web browser上有什麼媒體(e.g. video, audio),媒體的解像度,支援什麼解碼器等資訊。
  • 由於不同的網路config 和保安問題,在網路上的兩台設備未必能夠直接連線去進行video conferencing。在網路上找出兩台設備之間的直接連線這個技巧就是Interactive Connectivity Establishment(ICE),而以下2種中介server 就是用來做這個動作的:
    • STUN Server:它的作用是找出網路上能使這兩台設備之間可以直接connect的連線資訊(即是IP 和port),而每一個連線資訊就是一個ICE candidate。
    • TURN Server:如果STUN Server找不到這兩台設備之間的直接連線,TURN server 就充當"中間人"的角色,負責將video stream 做relay.
    • Signal server:有了兩台設備的連線資訊之後, 還需要Signal server 來幫這兩台設備來交換雙方連線資訊和local/remote description,使這兩台設備能夠順利連上。

Reference:
https://www.w3.org/TR/webrtc/

All images on this page are referred to:
https://medium.com/av-transcode/what-is-webrtc-and-how-to-setup-stun-turn-server-for-webrtc-communication-63314728b9d0

#javascript

#WebRTC

javascript string.replace 的進階用法

今日在stackoveflow 見到javascript string.replace 的進階用法,在此記錄一下。

一般來說string.replace 是用來做字串替換,例如:

let str="abc";
let result=str.replace('a','A');
console.log(result) //預期output Abc

string.replace 也支援regular expression,例如:

let str="abAc";

str=str.replace(/a/ig,"3");
console.log(str) //預期output 3b3c

根據MDN,原來也支援call back function:

const chars = {
  'a': 'x',
  'b': 'y',
  'c': 'z'
};

let s = '234abc567bbbbac';
s = s.replace(/[abc]/g, m => chars[m]);
console.log(s); //預期output 234xyz567yyyyxz

#javascript
#string.replace

javascript switch…..case 的特別用法

今日在stackoveflow 見到javascript switch….case 的比較特別用法,令我大開眼界,有感而發,不過現在先介紹一下傳統的javascript switch….case 用法。

一般來說javascript switch….case 用來解決因多重if…..then 使程式碼難讀的情況,例如:

if (a==1){
  .......
}else{
  if (a==2){
     .........
  }else{
     if (a==3){
        .........
     } 
  }
}

如果用switch….case來寫就會是變成以下這樣:

switch (a){
   case 1:
      ............   
      break
   case 2:
      ...........
      break
   case 3:
      .........
      break;
}

這樣一來,程式碼比較好讀,而且又少了因為層數太多而所產生的括弧配對問題。但是這檨寫法,每次a只能等於一value ,但如果想a 測試是否在一個範圍內就不行了,這是我一向的認知。

但是如果改用以下寫法就可以了:

switch (true){
   case (a>=1 && a<=3):
         ...............
         break;
   case (a>10 && a<20):
         .........
         break;  
}

最神奇是不一定同一個variable, 只要是想測試是不是true的expression 都可以了:

switch (true){
   case (a>=1 && a<=3):
         ...............
         break;
   case (b>10 && c<20):
         .........
         break;
   case ((new Date()).getSecond.getSecond()==10):
         ................
         break
}

這樣一來真的將switch….case 的power 放大很多,寫了那麼多年javascript 都不知原來可以這樣的,真是長知識。

#javascript

#switch…..case

如何用javascript 來做西曆轉做農曆

最近在研究日期formatting,發覺原來javascript 一些build-in function,
可以做到將一個西曆object以農曆格式顯示出來,現在作一個記錄

要format 一個西曆object,我們需要用Intl.DateTimeFormat object,
這個object 有一個叫format 的method 可以用來format西曆object,
我們透過這個object 的constructor 來控制它的output, 詳情請按這裡

let theDate = new Date("2022-10-1");
let lunarDateFormatter = new Intl.DateTimeFormat('zh-TW', {
  calendar: 'chinese',
  dateStyle: "long",
  numberingSystem: "hanidec"
});
console.log(lunarDateFormatter.format(theDate));

如果用Chrome /Firefox browser 來執行以上的code, 輸出會是這樣:

二〇二二壬寅年九月初六

還可以取出天干地支:

let theDate = new Date("2022-10-1");
let lunarDateFormatter = new Intl.DateTimeFormat('zh-TW', {
  calendar: 'chinese',
  dateStyle: "long",
  numberingSystem: "hanidec"
});
console.log(lunarDateFormatter.formatToParts(theDate));

執行以上的code, 輸出會是這樣:

[{
  type: "relatedYear",
  value: "二〇二二"
}, {
  type: "yearName",
  value: "壬寅"
}, {
  type: "literal",
  value: "年"
}, {
  type: "month",
  value: "九月"
}, {
  type: "day",
  value: "初六"
}]

#javascript
#西曆轉做農曆