package com.github.kmkt.util.mjpeg;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousCloseException;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.nio.channels.NetworkChannel;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Socket から Raw MJPEG Stream をフレーム単位で読み出し、リスナを callback する
*
* License : MIT License
*/
public class RawMJPEGReceiver {
private static final Logger logger = LoggerFactory.getLogger(RawMJPEGReceiver.class);
private final ExecutorService execPool;
private final boolean ownExecPool;
private final InetSocketAddress listenEndpoint;
private volatile ListenCompletionListener listenCallback;
public RawMJPEGReceiver(InetSocketAddress listen) {
this(listen, null);
}
public RawMJPEGReceiver(InetSocketAddress listen, ExecutorService pool) {
if (listen == null)
throw new IllegalArgumentException("listen should not be null");
if (pool != null) {
ownExecPool = false;
execPool = pool;
} else {
ownExecPool = true;
execPool = Executors.newCachedThreadPool(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
return t;
}
});
}
listenEndpoint = listen;
}
/**
* Socket accept 時の callback
*/
public interface ListenCompletionListener {
/**
* accept できた場合に callback される
* accept された socket からのデータを受け取るための ReceiveListener を返す
* null を返した場合は、受信されたデータは読み捨てされる
*
* @param remote リモートアドレス
* @return
*/
ReceiveListener<?> accepted(SocketAddress remote);
/**
* accept に失敗した場合に callback される
* @param e 失敗要因となった例外
*/
void failed(Throwable e);
}
/**
* Socket での MJPEG フレーム受信時に呼び出される callback
*
* @param <T>
*/
public static abstract class ReceiveListener<T> {
private T attachment;
/**
* コンストラクタ
* @param attachment callback時に付与される任意のオブジェクト
*/
public ReceiveListener(T attachment) {
this.attachment = attachment;
}
/**
* Socket での MJPEG フレーム受信時に呼び出される callback
*
* @param frame 受信された MJPEG フレーム (JPEGフレーム)
* @param attachement コンストラクタで与えたオブジェクト
*/
public abstract void onReceive(byte[] frame, T attachement);
/**
* Socket close 時に呼び出される callback
* @param attachement
*/
public abstract void onClose(T attachement);
void onReceive(byte[] frame) {
this.onReceive(frame, this.attachment);
}
void onClose() {
this.onClose(this.attachment);
}
}
public void setCallback(ListenCompletionListener callback) {
this.listenCallback = callback;
}
private Set<NetworkChannel> activeChannels = Collections.synchronizedSet(new HashSet<NetworkChannel>());
private AsynchronousServerSocketChannel assc = null;
public void start() throws IOException {
if (assc != null)
return;
assc = AsynchronousServerSocketChannel.open().bind(listenEndpoint);
assc.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(final AsynchronousSocketChannel result,
Void attachment) {
assc.accept(null, this);
try {
SocketAddress remote = result.getRemoteAddress();
ListenCompletionListener local_listener = listenCallback;
final ReceiveListener<?> listen;
if (local_listener != null) {
listen = local_listener.accepted(remote);
if (listen == null) {
logger.debug("Ignore and close connection from {}", remote);
result.close();
return;
}
} else {
listen = null;
}
execPool.execute(new Runnable() {
@Override
public void run() {
FrameParser parser = new FrameParser(8*1024*1024);
ByteBuffer buffer = parser.getByteBuffer();
activeChannels.add(result);
try {
while (result.isOpen()) {
// 受信
if (result.read(buffer).get() < 0) {
break; // EoS
}
byte[] frame = null;
while ((frame = parser.getFrame()) != null) {
if (listen != null) {
listen.onReceive(frame);
}
}
}
} catch (InterruptedException e) {
logger.debug("Exception occured when SocketChannel reading", e);
} catch (ExecutionException e) {
if (assc.isOpen()) {
logger.error("Exception occured when SocketChannel reading", e);
}
} finally {
try {
result.close();
activeChannels.remove(result);
} catch (IOException e) {
logger.error("Exception occured when SocketChannel closing", e);
}
if (listen != null) {
listen.onClose();
}
}
}
});
} catch (IOException e) {
this.failed(e, attachment);
try {
result.close();
} catch (IOException e1) {
logger.error("Exception occured when SocketChannel closing", e1);
}
}
}
@Override
public void failed(Throwable e, Void attachment) {
// close時に出るAsynchronousCloseException は無視
if (!(e instanceof AsynchronousCloseException)) {
listenCallback.failed(e);
}
}
});
}
public void stop() throws IOException {
if (ownExecPool) {
execPool.shutdown();
}
if (assc != null) {
assc.close();
}
synchronized (activeChannels) {
for (NetworkChannel soc : activeChannels) {
try {
soc.close();
} catch (IOException e) {
logger.error("Error at processing a socket of " + soc.getLocalAddress(), e);
}
}
activeChannels.clear();
}
}
}