html5 video tag + vertx pump

985 views
Skip to first unread message

Jekaterina Tjukova

unread,
May 23, 2014, 5:35:58 AM5/23/14
to ve...@googlegroups.com
Hi, 
I am trying to use Vertx Pump, to send a video file from server to client ensuring correct video playback with html5 video tag. 

The playback is fine when playing small video files without using the Pump, for instance:
Buffer buffer = vertx.fileSystem().readFileSync(pathToFile);
req.response().write(buffer.getBuffer(start, fileSize);

But this does not work with larger video files due to memory overload.


Using the Pump my code looks as follows:
vertx.fileSystem().open(pathToFile, new Handler<AsyncResult<AsyncFile>>() {
            @Override
            public void handle(AsyncResult<AsyncFile> fileResult) {
                if (fileResult.succeeded()) {
                    final AsyncFile asyncFile = fileResult.result();

                    Buffer buff = new Buffer();
                    final Pump pump = pumpGenerator.getPump(asyncFile.read(buff, 0, startByte, fileSize - startByte, new AsyncResultHandler<Buffer>() {
                        public void handle(AsyncResult<Buffer> ar) {
                            if (ar.succeeded()) {
                                LOG.info("Read file chunk OK");
                            } else {
                                LOG.error("Failed to write", ar.cause());
                            }
                        }
                    }), req.response()).start();
                    asyncFile.endHandler(new Handler<Void>() {
                        @Override
                        public void handle(Void event) {
                            req.response().end();
                        }
                    });
                }
            }
        });

This works OK with the small video (when a single HTTP request is enough to load the full video). With slightly bigger files, when the browser sends several requests with Http Header "Range" (i.e. 
Range:
bytes=673223804-), this causes Broken Pipe exception coming from client side (video tag), as if the sent data is corrupt or does not correspond to the Http response headers (i.e. Content-Length, Content-Range). As far as I understand the pump sends the file bytewise, so the response might actually be inaccurate. What is the right solution in this case?

Stack:
Schwerwiegend: Exception in Java verticle
java.io.IOException: Datenübergabe unterbrochen (broken pipe)
at sun.nio.ch.FileDispatcherImpl.write0(Native Method)
at sun.nio.ch.SocketDispatcher.write(SocketDispatcher.java:47)
at sun.nio.ch.IOUtil.writeFromNativeBuffer(IOUtil.java:93)
at sun.nio.ch.IOUtil.write(IOUtil.java:51)
at sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:487)
at io.netty.buffer.PooledUnsafeDirectByteBuf.getBytes(PooledUnsafeDirectByteBuf.java:208)
at io.netty.buffer.PooledUnsafeDirectByteBuf.readBytes(PooledUnsafeDirectByteBuf.java:215)
at io.netty.channel.socket.nio.NioSocketChannel.doWriteBytes(NioSocketChannel.java:214)
at io.netty.channel.nio.AbstractNioByteChannel.doWrite(AbstractNioByteChannel.java:194)
at io.netty.channel.socket.nio.NioSocketChannel.doWrite(NioSocketChannel.java:231)
at io.netty.channel.AbstractChannel$AbstractUnsafe.flush0(AbstractChannel.java:682)
at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.flush0(AbstractNioChannel.java:271)

Alexander Lehmann

unread,
May 23, 2014, 6:42:44 AM5/23/14
to ve...@googlegroups.com
Byte ranges are new requests, I assume that a Pump will not handle that correctly, that would require a seekable or skipable pump object.

The server can choose to ignore the byte ranges if it doesn't send Accept-Range: bytes, but I assume then the video tag will not work

You may be able to seek before starting the pump or you may have to write a file reader that doesn't use a pump, but serves the chunks directly.

Alexander Lehmann

unread,
May 26, 2014, 12:15:53 PM5/26/14
to ve...@googlegroups.com
Sorry, I take that back, the code to get the correct segment of the file in included in the async file reader.

Are you sure the response includes the correct range header matching the file segment?

Jekaterina Tjukova

unread,
May 27, 2014, 3:27:48 AM5/27/14
to ve...@googlegroups.com
Hi, thanks for support. I guess you're right, the content of the responses does not correspond to the headers, because asyncFile.read returns the results asynchronously, so in wrong order.

Jekaterina Tjukova

unread,
May 27, 2014, 5:41:48 AM5/27/14
to ve...@googlegroups.com
I have fixed this inconsistency - the first request asks for the whole video file, so I use req.response().sendFile() as reply for the first request from video tag (with Range: bytes=0-). Now the timing is OK, it was also not the problem as the content was mapped correctly to the responses.

Video playback is still impossible. I receive either Broken Pipe exception or "Die Verbindung wurde vom Kommunikationspartner zurückgesetzt".

Stack:
java.io.IOException: Die Verbindung wurde vom Kommunikationspartner zurückgesetzt

    at sun.nio.ch.FileDispatcherImpl.write0(Native Method)
    at sun.nio.ch.SocketDispatcher.write(SocketDispatcher.java:47)
    at sun.nio.ch.IOUtil.writeFromNativeBuffer(IOUtil.java:93)
    at sun.nio.ch.IOUtil.write(IOUtil.java:51)
    at sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:487)
    at io.netty.buffer.PooledUnsafeDirectByteBuf.getBytes(PooledUnsafeDirectByteBuf.java:208)
    at io.netty.buffer.PooledUnsafeDirectByteBuf.readBytes(PooledUnsafeDirectByteBuf.java:215)
    at io.netty.channel.socket.nio.NioSocketChannel.doWriteBytes(NioSocketChannel.java:214)
    at io.netty.channel.nio.AbstractNioByteChannel.doWrite(AbstractNioByteChannel.java:194)
    at io.netty.channel.socket.nio.NioSocketChannel.doWrite(NioSocketChannel.java:231)
    at io.netty.channel.AbstractChannel$AbstractUnsafe.flush0(AbstractChannel.java:682)
    at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.flush0(AbstractNioChannel.java:271)
    at io.netty.channel.AbstractChannel$AbstractUnsafe.flush(AbstractChannel.java:651)
    at io.netty.channel.DefaultChannelPipeline$HeadHandler.flush(DefaultChannelPipeline.java:1038)
    at io.netty.channel.DefaultChannelHandlerContext.invokeFlush(DefaultChannelHandlerContext.java:675)
    at io.netty.channel.DefaultChannelHandlerContext.flush(DefaultChannelHandlerContext.java:656)
    at io.netty.channel.ChannelOutboundHandlerAdapter.flush(ChannelOutboundHandlerAdapter.java:115)
    at io.netty.channel.DefaultChannelHandlerContext.invokeFlush(DefaultChannelHandlerContext.java:675)
    at io.netty.channel.DefaultChannelHandlerContext.flush(DefaultChannelHandlerContext.java:656)
    at io.netty.channel.ChannelDuplexHandler.flush(ChannelDuplexHandler.java:117)
    at io.netty.channel.DefaultChannelHandlerContext.invokeFlush(DefaultChannelHandlerContext.java:675)
    at io.netty.channel.DefaultChannelHandlerContext.write(DefaultChannelHandlerContext.java:701)
    at io.netty.channel.DefaultChannelHandlerContext.writeAndFlush(DefaultChannelHandlerContext.java:689)
    at io.netty.channel.DefaultChannelHandlerContext.writeAndFlush(DefaultChannelHandlerContext.java:718)
    at io.netty.channel.DefaultChannelPipeline.writeAndFlush(DefaultChannelPipeline.java:893)
    at io.netty.channel.AbstractChannel.writeAndFlush(AbstractChannel.java:240)
    at org.vertx.java.core.net.impl.ConnectionBase.write(ConnectionBase.java:82)
    at org.vertx.java.core.http.impl.ServerConnection.write(ServerConnection.java:122)
    at org.vertx.java.core.http.impl.DefaultHttpServerResponse.write(DefaultHttpServerResponse.java:472)
    at org.vertx.java.core.http.impl.DefaultHttpServerResponse.write(DefaultHttpServerResponse.java:221)
    at org.vertx.java.core.http.impl.DefaultHttpServerResponse.write(DefaultHttpServerResponse.java:41)
    at org.vertx.java.core.streams.Pump$2.handle(Pump.java:101)
    at org.vertx.java.core.streams.Pump$2.handle(Pump.java:99)
    at org.vertx.java.core.file.impl.DefaultAsyncFile.handleData(DefaultAsyncFile.java:292)
    at org.vertx.java.core.file.impl.DefaultAsyncFile.access$900(DefaultAsyncFile.java:48)
    at org.vertx.java.core.file.impl.DefaultAsyncFile$3.handle(DefaultAsyncFile.java:240)
    at org.vertx.java.core.file.impl.DefaultAsyncFile$3.handle(DefaultAsyncFile.java:229)
    at org.vertx.java.core.impl.DefaultFutureResult.checkCallHandler(DefaultFutureResult.java:122)
    at org.vertx.java.core.impl.DefaultFutureResult.setHandler(DefaultFutureResult.java:96)
    at org.vertx.java.core.file.impl.DefaultAsyncFile$6$1.run(DefaultAsyncFile.java:387)
    at org.vertx.java.core.impl.DefaultContext$3.run(DefaultContext.java:176)
    at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:354)
    at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:353)
    at io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:101)
    at java.lang.Thread.run(Thread.java:744)
Message has been deleted

Jekaterina Tjukova

unread,
May 27, 2014, 9:27:53 AM5/27/14
to ve...@googlegroups.com
UPDATE:
Pump in my initial code was sending the whole file, instead of just sending the required part. I should start the pump inside asyncFile.read() handler, but as Buffer does not extend ReadStream, I can't do that. So I guess I have to imitate Pump logic writing a Buffer into the response.

The code below is somewhat better, but the request should be properly ended.
vertx.fileSystem().open(
pathToFile, new Handler<AsyncResult<AsyncFile>>() {
            @Override
            public void handle(AsyncResult<AsyncFile> fileResult) {
                if (fileResult.succeeded()) {
                    final AsyncFile asyncFile = fileResult.result();
                    final int bufferSize = 100000;
                    for (int i = (int) startByte; i < fileSize; i+=bufferSize) {
                        final int position = i;
                        final Buffer buff = new Buffer(bufferSize);
                        asyncFile.read(buff, 0, i, bufferSize, new Handler<AsyncResult<Buffer>>() {
                            @Override
                            public void handle(AsyncResult<Buffer> buffer) {
                                req.response().write(buffer.result());
                            }
                        });
                    }
                }
            }
        });



On Friday, May 23, 2014 11:35:58 AM UTC+2, Jekaterina Tjukova wrote:

Alexander Lehmann

unread,
May 27, 2014, 11:59:23 AM5/27/14
to ve...@googlegroups.com
Connection reset by peer (Die Verbindung wurde vom Kommunikationspartner zurückgesetzt) means that the server has closed the connection but the client still expects data or the other way around, in the case of http requests, you are probably sending too few bytes than the other side expects, broken pipe is the other way around, the sending side sends data even though the other side has closed the connection already.

The Buffer/ReadStream problem is probably an issue with interfaces vs. classes, ReadStream and NetSocket are interfaces that are available in the respective object that is a subclass of Buffer, that should be possible to get the right interface if the object is of the correct kind with instanceof and cast (I think, haven't tried that yet).

I am at a bit of a loss how to properly test that, it should be possible to do a test connection with ncat and just request a few bytes at the end of a file and check what the result headers are. If you have a sample project that shows your issue, preferably including the player html page, that doesn't contain any proprietary elements, it would be good if you put that up on github or somewhere.

Jekaterina Tjukova

unread,
May 28, 2014, 8:28:43 AM5/28/14
to ve...@googlegroups.com
thanx for the infos. yes, "broken pipe" and "connection reset by peer" where due to wrong byte amount in the response. i have made a project, it would be nice if you take a look. the links to video examples are in the readme. let me know if something is missing.

https://github.com/jtjukova/html5video-vertx

Jekaterina Tjukova

unread,
May 28, 2014, 8:42:22 AM5/28/14
to ve...@googlegroups.com
So, currently the small video is loaded and played without problem, The playback of bigger videos is impossible, but the "poster" is sometimes loaded. I get numerous (possibly one for each req.response().write()) exceptions in Verticle stating:

Schwerwiegend: Exception in Java verticle
java.nio.channels.ClosedChannelException

Alexander Lehmann

unread,
May 28, 2014, 7:08:17 PM5/28/14
to ve...@googlegroups.com
Ok, I have found 2 different issues, the first is not really related to the program, the 2nd is probably the reason why this doesn't work as expected.

First the mostly unrelated issue, the file big.mp4 is encoded in a way that causes problems in the browser (at least it does in firefox), either this is because the file contains more than one audio stream or the resolution is too large, the actual size is 4k. The file doesn't play in the browser when it is dragged into the browser directly either. It looks like it works in Chrome, maybe their video codec implementation is better. I have exchanged the file with a webm movie from the Tears of Steel project that is also about 500 mb long.

The real issue is coming from the way the data is read and written in the verticle, the way the asynchronous operation works, it is not possible to process the read/write requests in a sequence, since they all get processed immediately after each other in the loop. If you add a logging statement before the asyncFile.read call, you will see that the segments are all read within a few seconds, even though the stream is not read by the client that fast.
I think it would be necessary to read/write the next segment each time when the previous segment was sent to the client, which I think is exactly what the Pump does, it waits for the writing buffer to drain a bit and then reads and sends the next bit of data.

The broken pipe or connection reset by peer exceptions are to be expected when the client closes the connection (which is allowed in http, even though this will give an error message in most servers, the reason why this doesn't show for the first request is that sendfile probably ignores this error silently), so one exception during writing is ok, this can be caught with an exception handler, but since in our case, each segment of the file is already waiting to be written, the exception will show for each segment that is not yet processed, in the case of a 500+mb file, this will be very many. If the segments are processed one after another, only one write operation will get an exception and after that the processing can simply be stopped.

Not quite sure how to implement that, I guess you need an endHandler that calls the next read operations.

Alexander Lehmann

unread,
Jun 6, 2014, 6:40:05 PM6/6/14
to ve...@googlegroups.com
Getting back to your first example, I found why the result returned by the pump code doesn't work.
Even though one might expect it, asyncFile.read() does not return an AsyncFile object that reads the partial file, which can be used in a pump, it returns just the same object to be able to chain method calls together that are all applied to the object. This means that the range request returns the complete file and not just the end of it as specified by the range, so the file is invalid (plus the file is too long for the content-length).

To be able to use a range request with a pump, it is probably necessary to write a AsyncFile object that reads a specified section of the file and implements the ReadStream interface.
...

Jekaterina Tjukova

unread,
Jun 12, 2014, 7:50:57 AM6/12/14
to ve...@googlegroups.com

Jekaterina Tjukova

unread,
Jun 13, 2014, 3:04:04 AM6/13/14
to ve...@googlegroups.com
UPDATE:
just for info: we fixed it using a recursive function, writing the chunks of file into the response
at sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:487)</
...
Reply all
Reply to author
Forward
0 new messages