Sunday, 23 May 2021

Share Webcam Servlet

I need to implement a oldschool live web-converence system without websocket without comet. Starting with POC I need to have some kind of buffer(max 40kb) filled by one camera using multiple POST requests having the video-chunks.

Having multiple GET requests to that servlet should copy all chunks to that response using chunked transfer-encoding.

This is the javascript

function start(){
    var opts = {
        video: {deviceId:val('cams')},
        audio: {deviceId:val('mics')}
    }; 
    navigator.mediaDevices.getUserMedia(opts).then(function(stream) {
        var r = new MediaRecorder(stream);
        r.ondataavailable = function(d){
            var type = d.type;
            if (type=='dataavailable') {
                var data = d.data;
                console.log(d);
                var blob = data.arrayBuffer();
                blob.then(function(d){
                    var x = new X();
                    x.open('POST','video');
                    x.setRequestHeader('X-Chunk-Type', data.type);
                    x.setRequestHeader('X-Timecode', d.timecode);
                    x.send(new Int8Array(d));
                });
            }
        }
        r.start(100);
    }).catch(function(err) {
        document.disabledDevices.push(opts.video.deviceId);
        reloadIO();
    });
}

This is my servlet bound to /video

public class VideoEcho implements Servlet {
    private ServletConfig config;
    private final ChunkBuffer buffer = new ChunkBuffer(1024 * 40); // 40kb

    @Override
    public void init(ServletConfig config) throws ServletException {
        this.config = config;
    }

    @Override
    public ServletConfig getServletConfig() {
        return config;
    }

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        if (req instanceof HttpServletRequest && res instanceof HttpServletResponse) {
            HttpServletRequest r = (HttpServletRequest) req;
            HttpServletResponse resp = (HttpServletResponse) res;
            if ("POST".equals(r.getMethod())) {
                buffer.addChunk(r);
            } else {
                buffer.getIncomminListeners().add(new ReplyIncommingChunk(resp));
                while (true)
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

            }
        }
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {
    }
}

ChunkBuffer is this:

public class ChunkBuffer {

    public interface ChunkIncommingListeners {
        void incomming(byte[] data);
    }

    private final int maxBufferSize;
    private int bytesInUse;
    private final byte[][] cache;
    private int cacheItems = 0;
    private final Set<ChunkIncommingListeners> incomminListeners = new LinkedHashSet<ChunkIncommingListeners>();

    public Set<ChunkIncommingListeners> getIncomminListeners() {
        return incomminListeners;
    }

    public ChunkBuffer(int maxBufferSize) {
        this.maxBufferSize = maxBufferSize;
        this.cache = new byte[maxBufferSize][];
    }

    public void addChunk(HttpServletRequest req) {
        int size = req.getContentLength();
        byte[] chunk = new byte[size];
        try {
            req.getInputStream().read(chunk);
            addNewest(chunk);
            this.bytesInUse += size;
            for (ChunkIncommingListeners chunkIncommingListeners : incomminListeners) {
                new Thread(new Runnable() {

                    @Override
                    public void run() {
                        chunkIncommingListeners.incomming(chunk);
                    }
                }).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        while (bytesInUse > maxBufferSize) {
            removeOldest();
        }
    }

    private void addNewest(byte[] chunk) {
        cache[cacheItems] = chunk;
        cacheItems++;
    }

    private void removeOldest() {
        bytesInUse -= cache[0].length;
        System.arraycopy(cache, 1, cache, 0, cacheItems);
        cacheItems--;
    }
}

and ReplyIncommingChunk is this:

public class ReplyIncommingChunk implements ChunkIncommingListeners {

    private HttpServletResponse res;
    private ServletOutputStream outputStream;

    public ReplyIncommingChunk(HttpServletResponse res) {
        this.res = res;
        try {
            res.setHeader("Transfer-Encoding", "chunked");
            res.setContentType("video/x-matroska;codecs=avc1,opus");
            outputStream = res.getOutputStream();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    @Override
    public void incomming(byte[] data) {
        try {
            String hex = Integer.toHexString(data.length) + "\r\n";
            outputStream.write(hex.getBytes(StandardCharsets.US_ASCII));
            outputStream.write(data);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

Problem:

If I use this HTML

<video src="https://localhost:8443/video" controls="true"></video>

I can neither see the video nor see any communication using the Chrome-Devtools that any bytes are transported using the GET-Request.

Any idea?



from Share Webcam Servlet

No comments:

Post a Comment