[FFmpeg-devel] [PATCH] HTTP: optimize forward seek performance

Joel Cunningham joel.cunningham at me.com
Thu Jan 12 01:01:08 EET 2017


Hi,

I’ve been working on optimizing HTTP forward seek performance for ffmpeg and would like to contribute this patch into mainline ffmpeg.  Please see the below patch for an explanation of the issue and proposed fix.  I have provided evidence of the current performance issue and my sample MP4 so others can reproduce and observe the behavior.

Files are located in Dropbox here: https://www.dropbox.com/sh/4q4ru8isdv22joj/AABU3XyXmgLMiEFqucf1LdZ3a?dl=0

GRMT0003.MP4 - test video file
mac-ffplay-baseline.pcapng - wireshark capture of ffplay (49abd) playing the above test file on MacOS 10.12.2 from a remote NGINX server
mac-ffplay-optimize-patch.pcapng - same ffplay setup but with patch applied 
ffplay_output.log - console output of ffplay with patch (loglevel debug)

I’m happy to discuss this issue further if the below description doesn’t fully communicate the issue.

Thanks,

Joel

From 89a3ed8aab9168313b4f7e83c00857f9b715ba4e Mon Sep 17 00:00:00 2001
From: Joel Cunningham <joel.cunningham at me.com>
Date: Wed, 11 Jan 2017 13:55:02 -0600
Subject: [PATCH] HTTP: optimize forward seek performance

This commit optimizes HTTP forward seeks by advancing the stream on
the current connection when the seek amount is within the current
TCP window rather than closing the connection and opening a new one.
This improves performance because with TCP flow control, a window's
worth of data is always either in the local socket buffer already or
in-flight from the sender.

The previous behavior of closing the connection, then opening a new
with a new HTTP range value results in a massive amounts of discarded
and re-sent data when large TCP windows are used.  This has been observed
on MacOS/iOS which starts with an inital window of 256KB and grows up to
1MB depending on the bandwidth-product delay.

When seeking within a window's worth of data and we close the connection,
then open a new one within the same window's worth of data, we discard
from the current offset till the end of the window.  Then on the new
connection the server ends up re-sending the previous data from new
offset till the end of old window.

Example:

TCP window size: 64KB
Position: 32KB
Forward seek position: 40KB

      *                      (Next window)
32KB |--------------| 96KB |---------------| 160KB
        *
  40KB |---------------| 104KB

Re-sent amount: 96KB - 40KB = 56KB

For a real world test example, I have MP4 file of ~25MB, which ffplay
only reads ~16MB and performs 177 seeks. With current ffmpeg, this results
in 177 HTTP GETs and ~73MB worth of TCP data communication.  With this
patch, ffmpeg issues 4 HTTP GETs for a total of ~20MB of TCP data
communication.

To support this feature, a new URL function has been added to get the
stream buffer size from the TCP protocol.  The stream advancement logic
has been implemented in the HTTP layer since this the layer in charge of
the seek and creating/destroying the TCP connections.

This feature has been tested on Windows 7 and MacOS/iOS.  Windows support
is slightly complicated by the fact that when TCP window auto-tuning is
enabled, SO_RCVBUF doesn't report the real window size, but it does if
SO_RCVBUF was manually set (disabling auto-tuning). So we can only use
this optimization on Windows in the later case
---
 libavformat/avio.c |  7 ++++++
 libavformat/http.c | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 libavformat/tcp.c  | 21 +++++++++++++++++
 libavformat/url.h  |  8 +++++++
 4 files changed, 105 insertions(+)

diff --git a/libavformat/avio.c b/libavformat/avio.c
index 3606eb0..34dcf09 100644
--- a/libavformat/avio.c
+++ b/libavformat/avio.c
@@ -645,6 +645,13 @@ int ffurl_get_multi_file_handle(URLContext *h, int **handles, int *numhandles)
     return h->prot->url_get_multi_file_handle(h, handles, numhandles);
 }
 
+int ffurl_get_stream_size(URLContext *h)
+{
+    if (!h->prot->url_get_stream_size)
+        return -1;
+    return h->prot->url_get_stream_size(h);
+}
+
 int ffurl_shutdown(URLContext *h, int flags)
 {
     if (!h->prot->url_shutdown)
diff --git a/libavformat/http.c b/libavformat/http.c
index 944a6cf..0026168 100644
--- a/libavformat/http.c
+++ b/libavformat/http.c
@@ -1462,6 +1462,61 @@ static int http_close(URLContext *h)
     return ret;
 }
 
+#define DISCARD_BUFFER_SIZE (256 * 1024)
+static int http_stream_forward(HTTPContext *s, int amount)
+{
+    int len;
+    int ret;
+    int discard = amount;
+    int discard_buf_size;
+    uint8_t * discard_buf;
+
+    /* advance local buffer first */
+    len = FFMIN(discard, s->buf_end - s->buf_ptr);
+    if (len > 0) {
+        av_log(s, AV_LOG_DEBUG, "advancing input buffer by %d bytes\n", len);
+        s->buf_ptr += len;
+        discard -= len;
+        if (discard == 0) {
+            return amount;
+        }
+    }
+
+    /* if underlying protocol is a stream with flow control and we are seeking
+    within the stream buffer size, it performs better to advance through the stream
+    rather than abort the connection and open a new one */
+    len = ffurl_get_stream_size(s->hd);
+    av_log(s, AV_LOG_DEBUG, "stream buffer size: %d\n", len);
+    if ((len <= 0) || (discard > len)) {
+        return AVERROR(EINVAL);
+    }
+
+    /* forward stream by discarding data till new position */
+    discard_buf_size = FFMIN(discard, DISCARD_BUFFER_SIZE);
+    discard_buf = av_malloc(discard_buf_size);
+    if (!discard_buf) {
+        return AVERROR(ENOMEM);
+    }
+
+    av_log(s, AV_LOG_DEBUG, "advancing stream by discarding %d bytes\n", discard);
+    while (discard > 0) {
+        ret = ffurl_read(s->hd, discard_buf, FFMIN(discard, discard_buf_size));
+        if (ret > 0) {
+            discard -= ret;
+        } else {
+            ret = (!ret ? AVERROR_EOF : ff_neterrno());
+            av_log(s, AV_LOG_DEBUG, "read error: %d\n", ret);
+            break;
+        }
+    }
+    av_freep(&discard_buf);
+
+    if (discard == 0) {
+        return amount;
+    }
+    return ret;
+}
+
 static int64_t http_seek_internal(URLContext *h, int64_t off, int whence, int force_reconnect)
 {
     HTTPContext *s = h->priv_data;
@@ -1493,6 +1548,20 @@ static int64_t http_seek_internal(URLContext *h, int64_t off, int whence, int fo
     if (s->off && h->is_streamed)
         return AVERROR(ENOSYS);
 
+    /* if seeking forward, see if we can satisfy the seek with our current connection */
+    if ((s->off > old_off) &&
+        (s->off - old_off <= INT32_MAX)) {
+        int amount = (int)(s->off - old_off);
+        int res;
+
+        av_log(s, AV_LOG_DEBUG, "attempting stream forwarding, old: %lld, new: %lld bytes: %d\n",
+           old_off, s->off, amount);
+        res = http_stream_forward(s, amount);
+        if (res == amount) {
+            return off;
+        }
+    }
+
     /* we save the old context in case the seek fails */
     old_buf_size = s->buf_end - s->buf_ptr;
     memcpy(old_buf, s->buf_ptr, old_buf_size);
diff --git a/libavformat/tcp.c b/libavformat/tcp.c
index 25abafc..b73e500 100644
--- a/libavformat/tcp.c
+++ b/libavformat/tcp.c
@@ -265,6 +265,26 @@ static int tcp_get_file_handle(URLContext *h)
     return s->fd;
 }
 
+static int tcp_get_window_size(URLContext *h)
+{
+    TCPContext *s = h->priv_data;
+    int avail;
+    int avail_len = sizeof(avail);
+
+    #if HAVE_WINSOCK2_H
+    /* SO_RCVBUF with winsock only reports the actual TCP window size when
+    auto-tuning has been disabled via setting SO_RCVBUF */
+    if (s->recv_buffer_size < 0) {
+        return AVERROR(EINVAL);
+    }
+    #endif
+
+    if (getsockopt(s->fd, SOL_SOCKET, SO_RCVBUF, &avail, &avail_len)) {
+           return ff_neterrno();
+    }
+    return avail;
+}
+
 const URLProtocol ff_tcp_protocol = {
     .name                = "tcp",
     .url_open            = tcp_open,
@@ -273,6 +293,7 @@ const URLProtocol ff_tcp_protocol = {
     .url_write           = tcp_write,
     .url_close           = tcp_close,
     .url_get_file_handle = tcp_get_file_handle,
+    .url_get_stream_size = tcp_get_window_size,
     .url_shutdown        = tcp_shutdown,
     .priv_data_size      = sizeof(TCPContext),
     .flags               = URL_PROTOCOL_FLAG_NETWORK,
diff --git a/libavformat/url.h b/libavformat/url.h
index 5c50245..5e607c9 100644
--- a/libavformat/url.h
+++ b/libavformat/url.h
@@ -84,6 +84,7 @@ typedef struct URLProtocol {
     int (*url_get_file_handle)(URLContext *h);
     int (*url_get_multi_file_handle)(URLContext *h, int **handles,
                                      int *numhandles);
+    int (*url_get_stream_size)(URLContext *h);
     int (*url_shutdown)(URLContext *h, int flags);
     int priv_data_size;
     const AVClass *priv_data_class;
@@ -249,6 +250,13 @@ int ffurl_get_file_handle(URLContext *h);
 int ffurl_get_multi_file_handle(URLContext *h, int **handles, int *numhandles);
 
 /**
+ * Return sizes (in bytes) of stream based URL buffer.
+ *
+ * @return size on success or <0 on error.
+ */
+int ffurl_get_stream_size(URLContext *h);
+
+/**
  * Signal the URLContext that we are done reading or writing the stream.
  *
  * @param h pointer to the resource
-- 
2.10.0


More information about the ffmpeg-devel mailing list